**...and more**
To learn more about TruffleHog and its features and capabilities, visit our [product page](https://trufflesecurity.com/trufflehog?gclid=CjwKCAjwouexBhAuEiwAtW_Zx5IW87JNj97Ci7heFnA5ar6-DuNzT2Y5nIl9DuZ-FOUqx0Qg3vb9nxoClcEQAvD_BwE).
# :globe_with_meridians: TruffleHog Enterprise
Are you interested in continuously monitoring **Git, Jira, Slack, Confluence, Microsoft Teams, Sharepoint (and more)** for credentials? We have an enterprise product that can help! Learn more at .
We take the revenue from the enterprise product to fund more awesome open source projects that the whole community can benefit from.
# What is TruffleHog 🐽
TruffleHog is the most powerful secrets **Discovery, Classification, Validation,** and **Analysis** tool. In this context, secret refers to a credential a machine uses to authenticate itself to another machine. This includes API keys, database passwords, private encryption keys, and more.
## Discovery 🔍
TruffleHog can look for secrets in many places including Git, chats, wikis, logs, API testing platforms, object stores, filesystems and more.
## Classification 📁
TruffleHog classifies over 800 secret types, mapping them back to the specific identity they belong to. Is it an AWS secret? Stripe secret? Cloudflare secret? Postgres password? SSL Private key? Sometimes it's hard to tell looking at it, so TruffleHog classifies everything it finds.
## Validation ✅
For every secret TruffleHog can classify, it can also log in to confirm if that secret is live or not. This step is critical to know if there’s an active present danger or not.
## Analysis 🔬
For the 20 some of the most commonly leaked out credential types, instead of sending one request to check if the secret can log in, TruffleHog can send many requests to learn everything there is to know about the secret. Who created it? What resources can it access? What permissions does it have on those resources?
# :loudspeaker: Join Our Community
Have questions? Feedback? Jump into Slack or Discord and hang out with us.
Join our [Slack Community](https://join.slack.com/t/trufflehog-community/shared_invite/zt-pw2qbi43-Aa86hkiimstfdKH9UCpPzQ)
Join the [Secret Scanning Discord](https://discord.gg/8Hzbrnkr7E)
# :tv: Demo

```bash
docker run --rm -it -v "$PWD:/pwd" trufflesecurity/trufflehog:latest github --org=trufflesecurity
```
# :floppy_disk: Installation
Several options are available for you:
### MacOS users
```bash
brew install trufflehog
```
### Docker:
_Ensure Docker engine is running before executing the following commands:_
#### Unix
```bash
docker run --rm -it -v "$PWD:/pwd" trufflesecurity/trufflehog:latest github --repo https://github.com/trufflesecurity/test_keys
```
#### Windows Command Prompt
```bash
docker run --rm -it -v "%cd:/=\%:/pwd" trufflesecurity/trufflehog:latest github --repo https://github.com/trufflesecurity/test_keys
```
#### Windows PowerShell
```bash
docker run --rm -it -v "${PWD}:/pwd" trufflesecurity/trufflehog github --repo https://github.com/trufflesecurity/test_keys
```
#### M1 and M2 Mac
```bash
docker run --platform linux/arm64 --rm -it -v "$PWD:/pwd" trufflesecurity/trufflehog:latest github --repo https://github.com/trufflesecurity/test_keys
```
### Binary releases
```bash
Download and unpack from https://github.com/trufflesecurity/trufflehog/releases
```
### Compile from source
```bash
git clone https://github.com/trufflesecurity/trufflehog.git
cd trufflehog; go install
```
### Using installation script
```bash
curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /usr/local/bin
```
### Using installation script, verify checksum signature (requires cosign to be installed)
```bash
curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -v -b /usr/local/bin
```
### Using installation script to install a specific version
```bash
curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /usr/local/bin
```
# :closed_lock_with_key: Verifying the artifacts
Checksums are applied to all artifacts, and the resulting checksum file is signed using cosign.
You need the following tool to verify signature:
- [Cosign](https://docs.sigstore.dev/cosign/system_config/installation/)
Verification steps are as follows:
1. Download the artifact files you want, and the following files from the [releases](https://github.com/trufflesecurity/trufflehog/releases) page.
- trufflehog\_{version}\_checksums.txt
- trufflehog\_{version}\_checksums.txt.pem
- trufflehog\_{version}\_checksums.txt.sig
2. Verify the signature:
```shell
cosign verify-blob \
--certificate \
--signature \
--certificate-identity-regexp 'https://github\.com/trufflesecurity/trufflehog/\.github/workflows/.+' \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
```
3. Once the signature is confirmed as valid, you can proceed to validate that the SHA256 sums align with the downloaded artifact:
```shell
sha256sum --ignore-missing -c trufflehog_{version}_checksums.txt
```
Replace `{version}` with the downloaded files version
Alternatively, if you are using the installation script, pass `-v` option to perform signature verification.
This requires Cosign binary to be installed prior to running the installation script.
# :rocket: Quick Start
## 1: Scan a repo for only verified secrets
Command:
```bash
trufflehog git https://github.com/trufflesecurity/test_keys --results=verified
```
Expected output:
```
🐷🔑🐷 TruffleHog. Unearth your secrets. 🐷🔑🐷
Found verified result 🐷🔑
Detector Type: AWS
Decoder Type: PLAIN
Raw result: AKIAYVP4CIPPERUVIFXG
Line: 4
Commit: fbc14303ffbf8fb1c2c1914e8dda7d0121633aca
File: keys
Email: counter
Repository: https://github.com/trufflesecurity/test_keys
Timestamp: 2022-06-16 10:17:40 -0700 PDT
...
```
## 2: Scan a GitHub Org for only verified secrets
```bash
trufflehog github --org=trufflesecurity --results=verified
```
## 3: Scan a GitHub Repo for only verified secrets and get JSON output
Command:
```bash
trufflehog git https://github.com/trufflesecurity/test_keys --results=verified --json
```
Expected output:
```
{"SourceMetadata":{"Data":{"Git":{"commit":"fbc14303ffbf8fb1c2c1914e8dda7d0121633aca","file":"keys","email":"counter \u003ccounter@counters-MacBook-Air.local\u003e","repository":"https://github.com/trufflesecurity/test_keys","timestamp":"2022-06-16 10:17:40 -0700 PDT","line":4}}},"SourceID":0,"SourceType":16,"SourceName":"trufflehog - git","DetectorType":2,"DetectorName":"AWS","DecoderName":"PLAIN","Verified":true,"Raw":"AKIAYVP4CIPPERUVIFXG","Redacted":"AKIAYVP4CIPPERUVIFXG","ExtraData":{"account":"595918472158","arn":"arn:aws:iam::595918472158:user/canarytokens.com@@mirux23ppyky6hx3l6vclmhnj","user_id":"AIDAYVP4CIPPJ5M54LRCY"},"StructuredData":null}
...
```
## 4: Scan a GitHub Repo + its Issues and Pull Requests
```bash
trufflehog github --repo=https://github.com/trufflesecurity/test_keys --issue-comments --pr-comments
```
## 5: Scan an S3 bucket for high-confidence results (verified + unknown)
```bash
trufflehog s3 --bucket= --results=verified,unknown
```
## 6: Scan S3 buckets using IAM Roles
```bash
trufflehog s3 --role-arn=
```
## 7: Scan a Github Repo using SSH authentication in Docker
```bash
docker run --rm -v "$HOME/.ssh:/root/.ssh:ro" trufflesecurity/trufflehog:latest git ssh://github.com/trufflesecurity/test_keys
```
## 8: Scan individual files or directories
```bash
trufflehog filesystem path/to/file1.txt path/to/file2.txt path/to/dir
```
## 9: Scan a local git repo
Clone the git repo. For example [test keys](git@github.com:trufflesecurity/test_keys.git) repo.
```bash
git clone git@github.com:trufflesecurity/test_keys.git
```
Run trufflehog from the parent directory (outside the git repo).
```bash
trufflehog git file://test_keys --results=verified,unknown
```
To guard against malicious git configs in local scanning (see CVE-2025-41390), TruffleHog clones local git repositories to a temporary directory prior to scanning. This follows [Git's security best practices](https://git-scm.com/docs/git#_security). If you want to specify a custom path to clone the repository to (instead of tmp), you can use the `--clone-path` flag. If you'd like to skip the local cloning process and scan the repository directly (only do this for trusted repos), you can use the `--trust-local-git-config` flag.
## 10: Scan GCS buckets for only verified secrets
```bash
trufflehog gcs --project-id= --cloud-environment --results=verified
```
## 11: Scan a Docker image for only verified secrets
Use the `--image` flag multiple times to scan multiple images.
```bash
# to scan from a remote registry
trufflehog docker --image trufflesecurity/secrets --results=verified
# to scan from the local docker daemon
trufflehog docker --image docker://new_image:tag --results=verified
# to scan from an image saved as a tarball
trufflehog docker --image file://path_to_image.tar --results=verified
```
## 12: Scan in CI
Set the `--since-commit` flag to your default branch that people merge into (ex: "main"). Set the `--branch` flag to your PR's branch name (ex: "feature-1"). Depending on the CI/CD platform you use, this value can be pulled in dynamically (ex: [CIRCLE_BRANCH in Circle CI](https://circleci.com/docs/variables/) and [TRAVIS_PULL_REQUEST_BRANCH in Travis CI](https://docs.travis-ci.com/user/environment-variables/)). If the repo is cloned and the target branch is already checked out during the CI/CD workflow, then `--branch HEAD` should be sufficient. The `--fail` flag will return an 183 error code if valid credentials are found.
```bash
trufflehog git file://. --since-commit main --branch feature-1 --results=verified,unknown --fail
```
## 13: Scan a Postman workspace
Use the `--workspace-id`, `--collection-id`, `--environment` flags multiple times to scan multiple targets.
```bash
trufflehog postman --token= --workspace-id=
```
## 14: Scan a Jenkins server
```bash
trufflehog jenkins --url https://jenkins.example.com --username admin --password admin
```
## 15: Scan an Elasticsearch server
### Scan a Local Cluster
There are two ways to authenticate to a local cluster with TruffleHog: (1) username and password, (2) service token.
#### Connect to a local cluster with username and password
```bash
trufflehog elasticsearch --nodes 192.168.14.3 192.168.14.4 --username truffle --password hog
```
#### Connect to a local cluster with a service token
```bash
trufflehog elasticsearch --nodes 192.168.14.3 192.168.14.4 --service-token ‘AAEWVaWM...Rva2VuaSDZ’
```
### Scan an Elastic Cloud Cluster
To scan a cluster on Elastic Cloud, you’ll need a Cloud ID and API key.
```bash
trufflehog elasticsearch \
--cloud-id 'search-prod:dXMtY2Vx...YjM1ODNlOWFiZGRlNjI0NA==' \
--api-key 'MlVtVjBZ...ZSYlduYnF1djh3NG5FQQ=='
```
## 16. Scan a GitHub Repository for Cross Fork Object References and Deleted Commits
The following command will enumerate deleted and hidden commits on a GitHub repository and then scan them for secrets. This is an alpha release feature.
```bash
trufflehog github-experimental --repo https://github.com//.git --object-discovery
```
In addition to the normal TruffleHog output, the `--object-discovery` flag creates two files in a new `$HOME/.trufflehog` directory: `valid_hidden.txt` and `invalid.txt`. These are used to track state during commit enumeration, as well as to provide users with a complete list of all hidden and deleted commits (`valid_hidden.txt`). If you'd like to automatically remove these files after scanning, please add the flag `--delete-cached-data`.
**Note**: Enumerating all valid commits on a repository using this method takes between 20 minutes and a few hours, depending on the size of your repository. We added a progress bar to keep you updated on how long the enumeration will take. The actual secret scanning runs extremely fast.
For more information on Cross Fork Object References, please [read our blog post](https://trufflesecurity.com/blog/anyone-can-access-deleted-and-private-repo-data-github).
## 17. Scan Hugging Face
### Scan a Hugging Face Model, Dataset or Space
```bash
trufflehog huggingface --model --space --dataset
```
### Scan all Models, Datasets and Spaces belonging to a Hugging Face Organization or User
```bash
trufflehog huggingface --org --user
```
(Optionally) When scanning an organization or user, you can skip an entire class of resources with `--skip-models`, `--skip-datasets`, `--skip-spaces` OR a particular resource with `--ignore-models `, `--ignore-datasets `, `--ignore-spaces `.
### Scan Discussion and PR Comments
```bash
trufflehog huggingface --model --include-discussions --include-prs
```
## 18. Scan stdin Input
```bash
aws s3 cp s3://example/gzipped/data.gz - | gunzip -c | trufflehog stdin
```
# :question: FAQ
- All I see is `🐷🔑🐷 TruffleHog. Unearth your secrets. 🐷🔑🐷` and the program exits, what gives?
- That means no secrets were detected
- Why is the scan taking a long time when I scan a GitHub org
- Unauthenticated GitHub scans have rate limits. To improve your rate limits, include the `--token` flag with a personal access token
- It says a private key was verified, what does that mean?
- A verified result means TruffleHog confirmed the credential is valid by testing it against the service's API. For private keys, we've confirmed the key can be used live for SSH or SSL authentication. Check out our Driftwood blog post to learn more [Blog post](https://trufflesecurity.com/blog/driftwood-know-if-private-keys-are-sensitive/)
- Is there an easy way to ignore specific secrets?
- If the scanned source [supports line numbers](https://github.com/trufflesecurity/trufflehog/blob/d6375ba92172fd830abb4247cca15e3176448c5d/pkg/engine/engine.go#L358-L365), then you can add a `trufflehog:ignore` comment on the line containing the secret to ignore that secrets.
# :newspaper: What's new in v3?
TruffleHog v3 is a complete rewrite in Go with many new powerful features.
- We've **added over 700 credential detectors that support active verification against their respective APIs**.
- We've also added native **support for scanning GitHub, GitLab, Docker, filesystems, S3, GCS, Circle CI and Travis CI**.
- **Instantly verify private keys** against millions of github users and **billions** of TLS certificates using our [Driftwood](https://trufflesecurity.com/blog/driftwood) technology.
- Scan binaries, documents, and other file formats
- Available as a GitHub Action and a pre-commit hook
## What is credential verification?
For every potential credential that is detected, we've painstakingly implemented programmatic verification against the API that we think it belongs to. Verification eliminates false positives and provides three result statuses:
- **verified**: Credential confirmed as valid and active by API testing
- **unverified**: Credential detected but not confirmed valid (may be invalid, expired, or verification disabled)
- **unknown**: Verification attempted but failed due to errors, such as a network or API failure
For example, the [AWS credential detector](pkg/detectors/aws/aws.go) performs a `GetCallerIdentity` API call against the AWS API to verify if an AWS credential is active.
# :memo: Usage
TruffleHog has a sub-command for each source of data that you may want to scan:
- git
- github
- gitlab
- docker
- s3
- filesystem (files and directories)
- syslog
- circleci
- travisci
- gcs (Google Cloud Storage)
- postman
- jenkins
- elasticsearch
- stdin
- multi-scan
Each subcommand can have options that you can see with the `--help` flag provided to the sub command:
```
$ trufflehog git --help
usage: TruffleHog [] [ ...]
TruffleHog is a tool for finding credentials.
Flags:
-h, --[no-]help Show context-sensitive help (also try --help-long and --help-man).
--log-level=0 Logging verbosity on a scale of 0 (info) to 5 (trace). Can be
disabled with "-1".
--[no-]profile Enables profiling and sets a pprof and fgprof server on :18066.
-j, --[no-]json Output in JSON format.
--[no-]json-legacy Use the pre-v3.0 JSON format. Only works with git, gitlab,
and github sources.
--[no-]github-actions Output in GitHub Actions format.
--concurrency=12 Number of concurrent workers.
--[no-]no-verification Don't verify the results.
--results=RESULTS Specifies which type(s) of results to output: verified (confirmed
valid by API), unknown (verification failed due to error),
unverified (detected but not verified), filtered_unverified
(unverified but would have been filtered out). Defaults to
verified,unverified,unknown.
--[no-]no-color Disable colorized output
--[no-]allow-verification-overlap
Allow verification of similar credentials across detectors
--[no-]filter-unverified Only output first unverified result per chunk per detector if there
are more than one results.
--filter-entropy=FILTER-ENTROPY
Filter unverified results with Shannon entropy. Start with 3.0.
--config=CONFIG Path to configuration file.
--[no-]print-avg-detector-time
Print the average time spent on each detector.
--[no-]no-update Don't check for updates.
--[no-]fail Exit with code 183 if results are found.
--[no-]fail-on-scan-errors
Exit with non-zero error code if an error occurs during the scan.
--verifier=VERIFIER ... Set custom verification endpoints.
--[no-]custom-verifiers-only
Only use custom verification endpoints.
--detector-timeout=DETECTOR-TIMEOUT
Maximum time to spend scanning chunks per detector (e.g., 30s).
--archive-max-size=ARCHIVE-MAX-SIZE
Maximum size of archive to scan. (Byte units eg. 512B, 2KB, 4MB)
--archive-max-depth=ARCHIVE-MAX-DEPTH
Maximum depth of archive to scan.
--archive-timeout=ARCHIVE-TIMEOUT
Maximum time to spend extracting an archive.
--include-detectors="all" Comma separated list of detector types to include. Protobuf name or
IDs may be used, as well as ranges.
--exclude-detectors=EXCLUDE-DETECTORS
Comma separated list of detector types to exclude. Protobuf name
or IDs may be used, as well as ranges. IDs defined here take
precedence over the include list.
--[no-]no-verification-cache
Disable verification caching
--[no-]force-skip-binaries
Force skipping binaries.
--[no-]force-skip-archives
Force skipping archives.
--[no-]skip-additional-refs
Skip additional references.
--user-agent-suffix=USER-AGENT-SUFFIX
Suffix to add to User-Agent.
--[no-]version Show application version.
Commands:
help [...]
Show help.
git []
Find credentials in git repositories.
github []
Find credentials in GitHub repositories.
github-experimental --repo=REPO []
Run an experimental GitHub scan. Must specify at least one experimental sub-module to run:
object-discovery.
gitlab --token=TOKEN []
Find credentials in GitLab repositories.
filesystem [] [...]
Find credentials in a filesystem.
s3 []
Find credentials in S3 buckets.
gcs []
Find credentials in GCS buckets.
syslog --format=FORMAT []
Scan syslog
circleci --token=TOKEN
Scan CircleCI
docker []
Scan Docker Image
travisci --token=TOKEN
Scan TravisCI
postman []
Scan Postman
elasticsearch []
Scan Elasticsearch
jenkins --url=URL []
Scan Jenkins
huggingface []
Find credentials in HuggingFace datasets, models and spaces.
stdin
Find credentials from stdin.
multi-scan
Find credentials in multiple sources defined in configuration.
json-enumerator [...]
Find credentials from a JSON enumerator input.
analyze
Analyze API keys for fine-grained permissions information.
```
For example, to scan a `git` repository, start with
```
trufflehog git https://github.com/trufflesecurity/trufflehog.git
```
## Configuration
TruffleHog supports defining [custom regex detectors](#custom-regex-detector-alpha)
and multiple sources in a configuration file provided via the `--config` flag.
The regex detectors can be used with any subcommand, while the sources defined
in configuration are only for the `multi-scan` subcommand.
The configuration format for sources can be found on Truffle Security's
[source configuration documentation page](https://docs.trufflesecurity.com/scan-data-for-secrets).
Example GitHub source configuration and [options reference](https://docs.trufflesecurity.com/github#Fvm1I):
```yaml
sources:
- connection:
'@type': type.googleapis.com/sources.GitHub
repositories:
- https://github.com/trufflesecurity/test_keys.git
unauthenticated: {}
name: example config scan
type: SOURCE_TYPE_GITHUB
verify: true
```
You may define multiple connections under the `sources` key (see above), and
TruffleHog will scan all of the sources concurrently.
## S3
The S3 source supports assuming IAM roles for scanning in addition to IAM users. This makes it easier for users to scan multiple AWS accounts without needing to rely on hardcoded credentials for each account.
The IAM identity that TruffleHog uses initially will need to have `AssumeRole` privileges as a principal in the [trust policy](https://aws.amazon.com/blogs/security/how-to-use-trust-policies-with-iam-roles/) of each IAM role to assume.
To scan a specific bucket using locally set credentials or instance metadata if on an EC2 instance:
```bash
trufflehog s3 --bucket=
```
To scan a specific bucket using an assumed role:
```bash
trufflehog s3 --bucket= --role-arn=
```
Multiple roles can be passed as separate arguments. The following command will attempt to scan every bucket each role has permissions to list in the S3 API:
```bash
trufflehog s3 --role-arn= --role-arn=
```
Exit Codes:
- 0: No errors and no results were found.
- 1: An error was encountered. Sources may not have completed scans.
- 183: No errors were encountered, but results were found. Will only be returned if `--fail` flag is used.
## :octocat: TruffleHog Github Action
### General Usage
```
on:
push:
branches:
- main
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Secret Scanning
uses: trufflesecurity/trufflehog@main
with:
extra_args: --results=verified,unknown
```
In the example config above, we're scanning for live secrets in all PRs and Pushes to `main`. Only code changes in the referenced commits are scanned. If you'd like to scan an entire branch, please see the "Advanced Usage" section below.
### Shallow Cloning
If you're incorporating TruffleHog into a standalone workflow and aren't running any other CI/CD tooling alongside TruffleHog, then we recommend using [Shallow Cloning](https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---depthltdepthgt) to speed up your workflow. Here's an example of how to do it:
```
...
- shell: bash
run: |
if [ "${{ github.event_name }}" == "push" ]; then
echo "depth=$(($(jq length <<< '${{ toJson(github.event.commits) }}') + 2))" >> $GITHUB_ENV
echo "branch=${{ github.ref_name }}" >> $GITHUB_ENV
fi
if [ "${{ github.event_name }}" == "pull_request" ]; then
echo "depth=$((${{ github.event.pull_request.commits }}+2))" >> $GITHUB_ENV
echo "branch=${{ github.event.pull_request.head.ref }}" >> $GITHUB_ENV
fi
- uses: actions/checkout@v3
with:
ref: ${{env.branch}}
fetch-depth: ${{env.depth}}
- uses: trufflesecurity/trufflehog@main
with:
extra_args: --results=verified,unknown
...
```
Depending on the event type (push or PR), we calculate the number of commits present. Then we add 2, so that we can reference a base commit before our code changes. We pass that integer value to the `fetch-depth` flag in the checkout action in addition to the relevant branch. Now our checkout process should be much shorter.
### Canary detection
TruffleHog statically detects [https://canarytokens.org/](https://canarytokens.org/).

### Advanced Usage
```yaml
- name: TruffleHog
uses: trufflesecurity/trufflehog@main
with:
# Repository path
path:
# Start scanning from here (usually main branch).
base:
# Scan commits until here (usually dev branch).
head: # optional
# Extra args to be passed to the trufflehog cli.
extra_args: --log-level=2 --results=verified,unknown
```
If you'd like to specify specific `base` and `head` refs, you can use the `base` argument (`--since-commit` flag in TruffleHog CLI) and the `head` argument (`--branch` flag in the TruffleHog CLI). We only recommend using these arguments for very specific use cases, where the default behavior does not work.
#### Advanced Usage: Scan entire branch
```
- name: scan-push
uses: trufflesecurity/trufflehog@main
with:
base: ""
head: ${{ github.ref_name }}
extra_args: --results=verified,unknown
```
## TruffleHog GitLab CI
### Example Usage
```yaml
stages:
- security
security-secrets:
stage: security
allow_failure: false
image: alpine:latest
variables:
SCAN_PATH: "." # Set the relative path in the repo to scan
before_script:
- apk add --no-cache git curl jq
- curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /usr/local/bin
script:
- trufflehog filesystem "$SCAN_PATH" --results=verified,unknown --fail --json | jq
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
```
In the example pipeline above, we're scanning for live secrets in all repository directories and files. This job runs only when the pipeline source is a merge request event, meaning it's triggered when a new merge request is created.
## Pre-commit Hook
TruffleHog can be used in a pre-commit hook to prevent credentials from leaking before they ever leave your computer.
See the [pre-commit hook documentation](PreCommit.md) for more information.
## Custom Regex Detector (alpha)
TruffleHog supports detection and verification of custom regular expressions.
For detection, at least one **regular expression** and **keyword** is required.
A **keyword** is a fixed literal string identifier that appears in or around
the regex to be detected. To allow maximum flexibility for verification, a
webhook is used containing the regular expression matches.
TruffleHog will send a JSON POST request containing the regex matches to a
configured webhook endpoint. If the endpoint responds with a `200 OK` response
status code, the secret is considered verified. If verification fails due to network/API errors, the result is marked as unknown.
Custom Detectors support a few different filtering mechanisms: entropy, regex targeting the entire match, regex targeting the captured secret,
and excluded word lists checked against the secret (captured group if present, entire match if capture group is not present). Note that if
your custom detector has multiple `regex` set (in this example `hogID`, and `hogToken`), then the filters get applied to each regex. [Here](examples/generic_with_filters.yml) is an example of a custom detector using these filters.
**NB:** This feature is alpha and subject to change.
### Regex Detector Example
[Here](/pkg/custom_detectors/CUSTOM_DETECTORS.md) is how to setup a custom regex detector with verification server.
## Generic JWT Detection
TruffleHog supports detection and verification of a subset of generic JWTs it finds.
Specifically, if a JWT uses public-key cryptography rather than HMAC and the public key can be obtained, TruffleHog can determine whether the JWT is live or not.
## :mag: Analyze
TruffleHog supports running a deeper analysis of a credential to view its permissions and the resources it has access to.
```bash
trufflehog analyze
```
# :heart: Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
# :computer: Contributing
Contributions are very welcome! Please see our [contribution guidelines first](CONTRIBUTING.md).
We no longer accept contributions to TruffleHog v2, but that code is available in the `v2` branch.
## Adding new secret detectors
We have published some [documentation and tooling to get started on adding new secret detectors](hack/docs/Adding_Detectors_external.md). Let's improve detection together!
# Use as a library
Currently, trufflehog is in heavy development and no guarantees can be made on
the stability of the public APIs at this time.
# License Change
Since v3.0, TruffleHog is released under a AGPL 3 license, included in [`LICENSE`](LICENSE). TruffleHog v3.0 uses none of the previous codebase, but care was taken to preserve backwards compatibility on the command line interface. The work previous to this release is still available licensed under GPL 2.0 in the history of this repository and the previous package releases and tags. A completed CLA is required for us to accept contributions going forward.
================================================
FILE: SECURITY.md
================================================
Please report security issues to security@trufflesec.com and include `trufflehog` in the subject line. If your vulnerability involves SSRF or outbound requests, please see our policy for that specific class of vulnerability below.
## Blind SSRF & Outbound Request Policy
Truffle Security treats blind SSRF (the ability to induce outbound requests without data retrieval) as a hardening opportunity rather than a vulnerability. We do not issue CVEs or formal advisories for reports showing outbound interactions unless they demonstrate a tangible security risk to users.
#### Policy Criteria
**Vulnerability (CVE Issued):** We will issue a CVE if a researcher demonstrates a clear exploit chain. For example:
- Credential Exfiltration: Forcing TruffleHog to send third-party secrets (discovered during a scan) or the host's own environment credentials (e.g., IAM metadata) to an attacker-controlled endpoint.
- Internal Exploitation: Using a blind request to trigger secondary vulnerabilities (e.g. RCE) on restricted internal services configured for defense-in-depth.
**Hardening (No CVE):** We generally will not issue a CVE for:
- Reflected Payloads: Inducing a request to an attacker-controlled URL that was already present in the scanned source code (i.e., the attacker receiving their own data back).
- Basic Outbound Control: Demonstrating control over the request URL, Path, or Body, without demonstrating a path to credential leakage or internal system exploitation.
- Service Probing: Simple open/closed port verification or basic interaction with internal services (e.g., triggering a GET request to a local web server) without a demonstrated compromise of data or system integrity.
- Secondary Vulnerability Dependencies: Where the impact relies entirely on the pre-existing lack of authentication, misconfiguration, or known vulnerabilities of a third-party internal service.
### Submission Guidelines
To help us evaluate your report, please specify:
- Level of Control: Which request components are controllable (Method, Host, Path, Headers, or Body)?
- Secret Context: Can you prove that a legitimate secret (not the attacker's payload) is attached to or contained within the outbound request?
- Target Reach: Can the request reach restricted internal IPs (e.g., 127.0.0.1 or 169.254.169.254)?
- Demonstrated Impact: What is the specific risk to a user or environment beyond a simple DNS/HTTP interaction?
================================================
FILE: action.yml
================================================
name: 'TruffleHog OSS'
description: 'Find and verify leaked credentials in your source code.'
author: Truffle Security Co.
inputs:
path:
description: Repository path
required: false
default: "./"
base:
description: Start scanning from here (usually main branch).
required: false
default: ""
head:
description: Scan commits until here (usually dev branch).
required: false
extra_args:
default: ""
description: Extra args to be passed to the trufflehog cli.
required: false
version:
default: "latest"
description: Scan with this trufflehog cli version.
required: false
branding:
icon: "shield"
color: "green"
runs:
using: "composite"
steps:
- shell: bash
working-directory: ${{ inputs.path }}
env:
BASE: ${{ inputs.base }}
HEAD: ${{ inputs.head }}
ARGS: ${{ inputs.extra_args }}
COMMIT_IDS: ${{ toJson(github.event.commits.*.id) }}
VERSION: ${{ inputs.version }}
run: |
##########################################
## ADVANCED USAGE ##
## Scan by BASE & HEAD user inputs ##
## If BASE == HEAD, exit with error ##
##########################################
# Check if jq is installed, if not, install it
if ! command -v jq &> /dev/null
then
echo "jq could not be found, installing..."
apt-get -y update && apt-get install -y jq
fi
git status >/dev/null # make sure we are in a git repository
if [ -n "$BASE" ] || [ -n "$HEAD" ]; then
if [ -n "$BASE" ]; then
base_commit=$(git rev-parse "$BASE" 2>/dev/null) || true
else
base_commit=""
fi
if [ -n "$HEAD" ]; then
head_commit=$(git rev-parse "$HEAD" 2>/dev/null) || true
else
head_commit=""
fi
if [ "$base_commit" == "$head_commit" ] ; then
echo "::error::BASE and HEAD commits are the same. TruffleHog won't scan anything. Please see documentation (https://github.com/trufflesecurity/trufflehog#octocat-trufflehog-github-action)."
exit 1
fi
##########################################
## Scan commits based on event type ##
##########################################
else
if [ "${{ github.event_name }}" == "push" ]; then
COMMIT_LENGTH=$(printenv COMMIT_IDS | jq length)
if [ $COMMIT_LENGTH == "0" ]; then
echo "No commits to scan"
exit 0
fi
HEAD=${{ github.event.after }}
if [ ${{ github.event.before }} == "0000000000000000000000000000000000000000" ]; then
BASE=""
else
BASE=${{ github.event.before }}
fi
elif [ "${{ github.event_name }}" == "workflow_dispatch" ] || [ "${{ github.event_name }}" == "schedule" ]; then
BASE=""
HEAD=""
elif [ "${{ github.event_name }}" == "pull_request" ]; then
BASE=${{github.event.pull_request.base.sha}}
HEAD=${{github.event.pull_request.head.sha}}
fi
fi
##########################################
## Run TruffleHog ##
##########################################
docker run --rm -v .:/tmp -w /tmp \
ghcr.io/trufflesecurity/trufflehog:${VERSION} \
git file:///tmp/ \
--since-commit \
${BASE:-''} \
--branch \
${HEAD:-''} \
--fail \
--no-update \
--github-actions \
${ARGS:-''}
================================================
FILE: docs/concurrency.md
================================================
## Concurrency
```mermaid
sequenceDiagram
%% Setup the workers
participant Main
Note over Main: e.startWorkers() kicks off some number of threads per worker type
create participant ScannerWorkers
Main->>ScannerWorkers: e.startScannerWorkers()
Note over ScannerWorkers: ScannerWorkers are primarily responsible for enumerating and chunking a source
create participant VerificationOverlapWorkers
Main->>VerificationOverlapWorkers: e.startVerificationOverlapWorkers()
Note over VerificationOverlapWorkers: VerificationOverlapWorkers handles chunks matched to multiple detectors
create participant DetectorWorkers
Main->>DetectorWorkers: e.startDetectorWorkers()
Note over DetectorWorkers: DetectorWorkers are primarily responsible for running detectors on chunks
create participant NotifierWorkers
Main->>NotifierWorkers: e.startNotifierWorkers()
Note over NotifierWorkers: Primarily responsible for reporting results (typically to the cmd line)
%% Set up the parallelism
par
Note over Main,ScannerWorkers: Depending on the type of scan requested, calls one of engine.(ScanGit|ScanGitHub|ScanFileSystem|etc)
Main->>ScannerWorkers: e.ChunksChan() <- chunk
and
Note over ScannerWorkers: Decode chunks and find matching detectors
ScannerWorkers->>DetectorWorkers: e.detectableChunksChan <- detectableChunk
Note over ScannerWorkers: When multiple detectors match on the same chunk we have to decided _which_ detector will verify found secrets
ScannerWorkers->>VerificationOverlapWorkers: e.verificationOverlapChunksChan <- verificationOverlapChunk
and
Note over VerificationOverlapWorkers: Decide which detectors to run on that chunk
VerificationOverlapWorkers->>DetectorWorkers: e.detectableChunksChan <- detectableChunk
and
Note over DetectorWorkers: Run detection (finding secrets), optionally verify them do filtering and enrichment
DetectorWorkers->>NotifierWorkers: e.ResultsChan()|e.results <-detectors.ResultWithMetadata
and
Note over NotifierWorkers: Write results to output
end
```
================================================
FILE: docs/iterative_decoding_performance.md
================================================
# Iterative Decoding Performance
Performance characteristics of the `--max-decode-depth` feature, which enables
chained decoding (e.g., base64 inside UTF-16, double-encoded base64).
## How it works
At depth 0, all decoders run on the original chunk (identical to pre-existing
behavior). When a decoder produces new output, that output is fed back through
all decoders at the next depth level. The loop exits early when no decoder
produces new data, so unused depth levels are effectively free.
The PLAIN (UTF-8) decoder is skipped at depth > 0 since it's a passthrough
that never transforms data produced by other decoders (their output is already
valid UTF-8/ASCII).
## Filesystem scan benchmark
Scanned the trufflehog repository (~4,500 files) with `--no-verification`
and `--concurrency=1` for deterministic comparison.
| Depth | Wall time | Unique results | Delta vs depth=1 |
|-------|-----------|----------------|-------------------|
| 1 | 8.05s | 924 | — |
| 2 | 8.18s | 927 | +3, +1.6% |
| 3 | 8.09s | 928 | +4, +0.5% |
| 5 | 8.19s | 928 | +4, +1.7% |
| 10 | 8.35s | 932 | +8, +3.7% |
Results converge by depth 3. Depths 4–5 produce no additional decoded data in
this corpus, so they add only a single `len() == 0` check per chunk per extra
depth level.
The small unique-result variance at depth 10 is from pre-existing
nondeterminism in the concurrent detector workers' dedup ordering, not from the
decoding itself.
## Per-decoder microbenchmarks
Individual decoder cost is unchanged by this feature (decoders are not
modified). For reference, base64 decoder latency on random data:
| Input size | Latency/op | Allocs |
|------------|------------|-----------|
| 100 B | ~250 ns | 96 B / 2 |
| 1 KB | ~2.25 µs | 96 B / 2 |
| 10 KB | ~44 ns | 96 B / 2 |
The 10 KB case is fast because random bytes rarely form valid base64 substrings
(the 20-character minimum threshold is never met), so the decoder exits after a
single O(n) character scan.
## Memory overhead
Each depth level that produces new decoded data stores one copy of the output
(typically smaller than the input, since base64 decoding shrinks by ~25%).
A `seen` list (slice of byte slices) prevents reprocessing identical data.
At depth 5 on a typical chunk, this list has 0–3 entries. No hashing or maps
are used.
## Choosing a depth
| Depth | Use case |
|-------|----------|
| 1 | Legacy behavior, no chaining |
| 2 | Covers base64-in-base64, base64-in-UTF-16, base64-in-escaped-unicode |
| 5 | Default. Handles deeply nested configs with no measurable cost over depth 2 |
================================================
FILE: docs/process_flow.md
================================================
# TruffleHog Process Flows
## Scans
## Data Flow
```mermaid
flowchart LR
SourceDecomposition["`**Source Decomposition**
Breaking up the locations that we are looking _for_ secrets into small chunks`"]
DetectorMatching{Chunk to Detector Matching}
SecretDetection["`**Secret Detection**
Finding secrets in these chunks and (optionally) verifying whether they are live`"]
ResultNotification["`**Result Notification**
Enriching results with metadata and (usually) printing to console`"]
SourceDecomposition -- chunks --> DetectorMatching
DetectorMatching -- matched chunks --> SecretDetection
SecretDetection -- results --> ResultNotification
```
#### Source Decomposition
```mermaid
flowchart TD
subgraph Source
direction TB
SourceDescription("`**(1)** Sources are top level places we find data/files/text to _scan_`")
GitSource["git Source"]
GitHubSource["GitHub Source"]
FilesystemSource["File System Source"]
PostmanSource["Postman Source"]
end
subgraph Unit
direction TB
UnitDescription("`**(2)** Units are natural subdivisions of Sources, but still quite large`")
FilesystemUnit[Directory]
GitUnit[Git Repository]
end
subgraph Chunk
direction TB
ChunkDescription("`**(3)** Chunks are the smallest units that we decompose our chunks into, and are subsequent passed on to detection`")
FilesystemChunk[file contents]
GitRepositoryChunk["`git log diff hunks`"]
PostmanChunk[data chunk]
end
SourceDescription -- decomposed into --> UnitDescription
UnitDescription -- further decomposed into --> ChunkDescription
GitSource -- cloned locally if not already local --> GitUnit
GitHubSource -- cloned locally --> GitUnit
PostmanSource -- Most sources\ndon't use units --> PostmanChunk
FilesystemSource --> FilesystemUnit
GitUnit -- git log -p --> GitRepositoryChunk
FilesystemUnit --> FilesystemChunk
style SourceDescription fill:#89553e
style UnitDescription fill:#89553e
style ChunkDescription fill:#89553e
```
#### Chunk to Detector Matching
```mermaid
flowchart LR
KeywordMatching["`**Keyword Matching**
_(Aho-Corasick)_
Match chunks to detectors based on the presence of specific keywords in the chunk`"]
chunks --> KeywordMatching --> detectors
```
#### Secret Detection
```mermaid
flowchart LR
subgraph Detector
direction RL
subgraph DetectorDescription[" "]
DetectorDescriptionText["`Detectors are the bits that actually check for the existence of a secret in a chunk, and (optionally) verify it`"]
ExampleDetectors["`Example Detectors:
* AWS
* Azure
* Twilio`"]
end
subgraph DetectorResponsibility[" "]
direction LR
De-Dupe-Detectors["`**De-Dupe-Detectors**
If multiple detectors keyword-match on the same chunk, we have some logic that chooses which detector will verify found secret (so we don't duplicate verification requests to external APIs)`"]
CollectMatches["`**Collect Matches**
Detector specific regexes are run against the matched chunks, resulting in unverified secrets`"]
VerifyMatches["`**Verify Matches**
Optionally, observed unverified secrets are verified by attempting to use them against live services`"]
De-Dupe-Detectors -- deduped detectors --> CollectMatches
CollectMatches -- regex matched chunks --> VerifyMatches
end
style DetectorDescription fill:#89553e
style DetectorDescriptionText fill:#89553e
end
```
#### Result Notification
```mermaid
flowchart LR
Dispatcher["`**Dispatcher**
Results, verified or otherwise, are sent to a dispatcher to be sent to whichever place we're updating about the
results -- usually the command line.`"]
results --> Dispatcher --> output
```
================================================
FILE: entrypoint.sh
================================================
#!/usr/bin/env bash
# Parse the last argument into an array of extra_args.
mapfile -t extra_args < <(bash -c "for arg in ${*: -1}; do echo \$arg; done")
# Directories might be owned by a user other than root
git config --global --add safe.directory '*'
if [[ $# -eq 0 ]]; then
/usr/bin/trufflehog --help
else
/usr/bin/trufflehog "${@: 1: $#-1}" "${extra_args[@]}"
fi
================================================
FILE: examples/README.md
================================================
# Examples
This folder contains various examples like custom detectors, scripts, etc. Feel free to contribute!
### Generic Detector
An often requested feature for TruffleHog is a generic detector. By default, we do not support generic detection as it would result in lots of false positives. However, if you want to attempt detect generic secrets you can use a custom detector.
#### Try it out:
```
wget https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/examples/generic.yml
trufflehog filesystem --config=$PWD/generic.yml $PWD
# to filter so that _only_ generic credentials are logged:
trufflehog filesystem --config=$PWD/generic.yml --json --no-verification $PWD | awk '/generic-api-key/{print $0}'
```
================================================
FILE: examples/generic.yml
================================================
detectors:
- name: generic-api-key
keywords:
- key
- api
- token
- secret
- client
- passwd
- password
- auth
- access
regex:
# borrowing the gitleaks generic-api-key regex
generic-api-key: "(?i)(?:key|api|token|secret|client|passwd|password|auth|access)(?:[0-9a-z\\-_\\t .]{0,20})(?:[\\s|']|[\\s|\"]){0,3}(?:=|>|:{1,3}=|\\|\\|:|<=|=>|:|\\?=)(?:'|\"|\\s|=|\\x60){0,5}([0-9a-z\\-_.=]{10,150})(?:['|\"|\\n|\\r|\\s|\\x60|;]|$)"
================================================
FILE: examples/generic_with_filters.yml
================================================
detectors:
- name: generic-password
keywords:
- pass
- access
- auth
- credential
- cred
- secret
- token
regex:
secret: |-
(?i)[\w.-]{0,50}?(?:access|auth|(?-i:[Aa]pi|API)|credential|creds|key|passw(?:or)?d|secret|token)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([\w.=-]{10,150}|[a-z0-9][a-z0-9+/]{11,}={0,3})(?:[\x60'"\s;]|\\[nr]|$)
validations:
secret: # name of the regex to apply these validations to
contains_digit: true
contains_special_char: true
entropy: 3.5
# exclude_regexes_capture:
# - |-
# (?i)(?:ignore)
exclude_regexes_match:
- |-
(?i)(?:access(?:ibility|or)|access[_.-]?id|random[_.-]?access|api[_.-]?(?:id|name|version)|rapid|capital|[a-z0-9-]*?api[a-z0-9-]*?:jar:|author|X-MS-Exchange-Organization-Auth|Authentication-Results|(?:credentials?[_.-]?id|withCredentials)|(?:bucket|foreign|hot|idx|natural|primary|pub(?:lic)?|schema|sequence)[_.-]?key|key[_.-]?(?:alias|board|code|frame|id|length|mesh|name|pair|ring|selector|signature|size|stone|storetype|word|up|down|left|right)|key[_.-]?vault[_.-]?(?:id|name)|keyVaultToStoreSecrets|key(?:store|tab)[_.-]?(?:file|path)|issuerkeyhash|(?-i:[DdMm]onkey|[DM]ONKEY)|keying|(?:secret)[_.-]?(?:length|name|size)|UserSecretsId|(?:io\.jsonwebtoken[ \t]?:[ \t]?[\w-]+)|(?:api|credentials|token)[_.-]?(?:endpoint|ur[il])|public[_.-]?token|(?:key|token)[_.-]?file|(?-i:(?:[A-Z_]+=\n[A-Z_]+=|[a-z_]+=\n[a-z_]+=)(?:\n|\z))|(?-i:(?:[A-Z.]+=\n[A-Z.]+=|[a-z.]+=\n[a-z.]+=)(?:\n|\z)))
exclude_words:
- "exclude"
- "000000"
- "aaaaaa"
- "about"
- "abstract"
- "academy"
- "acces"
- "account"
- "act-"
- "act."
- "act_"
- "action"
- "active"
- "actively"
- "activity"
- "adapter"
- "add-"
- "add."
- "add_"
- "add-on"
- "addon"
- "addres"
- "admin"
- "adobe"
- "advanced"
- "adventure"
- "agent"
- "agile"
- "air-"
- "air."
- "air_"
- "ajax"
- "akka"
- "alert"
- "alfred"
- "algorithm"
- "all-"
- "all."
- "all_"
- "alloy"
- "alpha"
- "amazon"
- "amqp"
- "analysi"
- "analytic"
- "analyzer"
- "android"
- "angular"
- "angularj"
- "animate"
- "animation"
- "another"
- "ansible"
- "answer"
- "ant-"
- "ant."
- "ant_"
- "any-"
- "any."
- "any_"
- "apache"
- "app-"
- "app-"
- "app."
- "app."
- "app_"
- "app_"
- "apple"
- "arch"
- "archive"
- "archived"
- "arduino"
- "array"
- "art-"
- "art."
- "art_"
- "article"
- "asp-"
- "asp."
- "asp_"
- "asset"
- "async"
- "atom"
- "attention"
- "audio"
- "audit"
- "aura"
- "auth"
- "author"
- "author"
- "authorize"
- "auto"
- "automated"
- "automatic"
- "awesome"
- "aws_"
- "azure"
- "back"
- "backbone"
- "backend"
- "backup"
- "bar-"
- "bar."
- "bar_"
- "base"
- "based"
- "bash"
- "basic"
- "batch"
- "been"
- "beer"
- "behavior"
- "being"
- "benchmark"
- "best"
- "beta"
- "better"
- "big-"
- "big."
- "big_"
- "binary"
- "binding"
- "bit-"
- "bit."
- "bit_"
- "bitcoin"
- "block"
- "blog"
- "board"
- "book"
- "bookmark"
- "boost"
- "boot"
- "bootstrap"
- "bosh"
- "bot-"
- "bot."
- "bot_"
- "bower"
- "box-"
- "box."
- "box_"
- "boxen"
- "bracket"
- "branch"
- "bridge"
- "browser"
- "brunch"
- "buffer"
- "bug-"
- "bug."
- "bug_"
- "build"
- "builder"
- "building"
- "buildout"
- "buildpack"
- "built"
- "bundle"
- "busines"
- "but-"
- "but."
- "but_"
- "button"
- "cache"
- "caching"
- "cakephp"
- "calendar"
- "call"
- "camera"
- "campfire"
- "can-"
- "can."
- "can_"
- "canva"
- "captcha"
- "capture"
- "card"
- "carousel"
- "case"
- "cassandra"
- "cat-"
- "cat."
- "cat_"
- "category"
- "center"
- "cento"
- "challenge"
- "change"
- "changelog"
- "channel"
- "chart"
- "chat"
- "cheat"
- "check"
- "checker"
- "chef"
- "ches"
- "chinese"
- "chosen"
- "chrome"
- "ckeditor"
- "clas"
- "classe"
- "classic"
- "clean"
- "cli-"
- "cli."
- "cli_"
- "client"
- "client"
- "clojure"
- "clone"
- "closure"
- "cloud"
- "club"
- "cluster"
- "cms-"
- "cms_"
- "coco"
- "code"
- "coding"
- "coffee"
- "color"
- "combination"
- "combo"
- "command"
- "commander"
- "comment"
- "commit"
- "common"
- "community"
- "compas"
- "compiler"
- "complete"
- "component"
- "composer"
- "computer"
- "computing"
- "con-"
- "con."
- "con_"
- "concept"
- "conf"
- "config"
- "config"
- "connect"
- "connector"
- "console"
- "contact"
- "container"
- "contao"
- "content"
- "contest"
- "context"
- "control"
- "convert"
- "converter"
- "conway'"
- "cookbook"
- "cookie"
- "cool"
- "copy"
- "cordova"
- "core"
- "couchbase"
- "couchdb"
- "countdown"
- "counter"
- "course"
- "craft"
- "crawler"
- "create"
- "creating"
- "creator"
- "credential"
- "crm-"
- "crm."
- "crm_"
- "cros"
- "crud"
- "csv-"
- "csv."
- "csv_"
- "cube"
- "cucumber"
- "cuda"
- "current"
- "currently"
- "custom"
- "daemon"
- "dark"
- "dart"
- "dash"
- "dashboard"
- "data"
- "database"
- "date"
- "day-"
- "day."
- "day_"
- "dead"
- "debian"
- "debug"
- "debug"
- "debugger"
- "deck"
- "define"
- "del-"
- "del."
- "del_"
- "delete"
- "demo"
- "deploy"
- "design"
- "designer"
- "desktop"
- "detection"
- "detector"
- "dev-"
- "dev."
- "dev_"
- "develop"
- "developer"
- "device"
- "devise"
- "diff"
- "digital"
- "directive"
- "directory"
- "discovery"
- "display"
- "django"
- "dns-"
- "dns_"
- "doc-"
- "doc-"
- "doc."
- "doc."
- "doc_"
- "doc_"
- "docker"
- "docpad"
- "doctrine"
- "document"
- "doe-"
- "doe."
- "doe_"
- "dojo"
- "dom-"
- "dom."
- "dom_"
- "domain"
- "done"
- "don't"
- "dot-"
- "dot."
- "dot_"
- "dotfile"
- "download"
- "draft"
- "drag"
- "drill"
- "drive"
- "driven"
- "driver"
- "drop"
- "dropbox"
- "drupal"
- "dsl-"
- "dsl."
- "dsl_"
- "dynamic"
- "easy"
- "_ec2_"
- "ecdsa"
- "eclipse"
- "edit"
- "editing"
- "edition"
- "editor"
- "element"
- "emac"
- "email"
- "embed"
- "embedded"
- "ember"
- "emitter"
- "emulator"
- "encoding"
- "endpoint"
- "engine"
- "english"
- "enhanced"
- "entity"
- "entry"
- "env_"
- "episode"
- "erlang"
- "error"
- "espresso"
- "event"
- "evented"
- "example"
- "example"
- "exchange"
- "exercise"
- "experiment"
- "expire"
- "exploit"
- "explorer"
- "export"
- "exporter"
- "expres"
- "ext-"
- "ext."
- "ext_"
- "extended"
- "extension"
- "external"
- "extra"
- "extractor"
- "fabric"
- "facebook"
- "factory"
- "fake"
- "fast"
- "feature"
- "feed"
- "fewfwef"
- "ffmpeg"
- "field"
- "file"
- "filter"
- "find"
- "finder"
- "firefox"
- "firmware"
- "first"
- "fish"
- "fix-"
- "fix_"
- "flash"
- "flask"
- "flat"
- "flex"
- "flexible"
- "flickr"
- "flow"
- "fluent"
- "fluentd"
- "fluid"
- "folder"
- "font"
- "force"
- "foreman"
- "fork"
- "form"
- "format"
- "formatter"
- "forum"
- "foundry"
- "framework"
- "free"
- "friend"
- "friendly"
- "front-end"
- "frontend"
- "ftp-"
- "ftp."
- "ftp_"
- "fuel"
- "full"
- "fun-"
- "fun."
- "fun_"
- "func"
- "future"
- "gaia"
- "gallery"
- "game"
- "gateway"
- "gem-"
- "gem."
- "gem_"
- "gen-"
- "gen."
- "gen_"
- "general"
- "generator"
- "generic"
- "genetic"
- "get-"
- "get."
- "get_"
- "getenv"
- "getting"
- "ghost"
- "gist"
- "git-"
- "git."
- "git_"
- "github"
- "gitignore"
- "gitlab"
- "glas"
- "gmail"
- "gnome"
- "gnu-"
- "gnu."
- "gnu_"
- "goal"
- "golang"
- "gollum"
- "good"
- "google"
- "gpu-"
- "gpu."
- "gpu_"
- "gradle"
- "grail"
- "graph"
- "graphic"
- "great"
- "grid"
- "groovy"
- "group"
- "grunt"
- "guard"
- "gui-"
- "gui."
- "gui_"
- "guide"
- "guideline"
- "gulp"
- "gwt-"
- "gwt."
- "gwt_"
- "hack"
- "hackathon"
- "hacker"
- "hacking"
- "hadoop"
- "haml"
- "handler"
- "hardware"
- "has-"
- "has_"
- "hash"
- "haskell"
- "have"
- "haxe"
- "hello"
- "help"
- "helper"
- "here"
- "hero"
- "heroku"
- "high"
- "hipchat"
- "history"
- "home"
- "homebrew"
- "homepage"
- "hook"
- "host"
- "hosting"
- "hot-"
- "hot."
- "hot_"
- "house"
- "how-"
- "how."
- "how_"
- "html"
- "http"
- "hub-"
- "hub."
- "hub_"
- "hubot"
- "human"
- "icon"
- "ide-"
- "ide."
- "ide_"
- "idea"
- "identity"
- "idiomatic"
- "image"
- "impact"
- "import"
- "important"
- "importer"
- "impres"
- "index"
- "infinite"
- "info"
- "injection"
- "inline"
- "input"
- "inside"
- "inspector"
- "instagram"
- "install"
- "installer"
- "instant"
- "intellij"
- "interface"
- "internet"
- "interview"
- "into"
- "intro"
- "ionic"
- "iphone"
- "ipython"
- "irc-"
- "irc_"
- "iso-"
- "iso."
- "iso_"
- "issue"
- "jade"
- "jasmine"
- "java"
- "jbos"
- "jekyll"
- "jenkin"
- "jetbrains"
- "job-"
- "job."
- "job_"
- "joomla"
- "jpa-"
- "jpa."
- "jpa_"
- "jquery"
- "json"
- "just"
- "kafka"
- "karma"
- "kata"
- "kernel"
- "keyboard"
- "kindle"
- "kit-"
- "kit."
- "kit_"
- "kitchen"
- "knife"
- "koan"
- "kohana"
- "lab-"
- "lab-"
- "lab."
- "lab."
- "lab_"
- "lab_"
- "lambda"
- "lamp"
- "language"
- "laravel"
- "last"
- "latest"
- "latex"
- "launcher"
- "layer"
- "layout"
- "lazy"
- "ldap"
- "leaflet"
- "league"
- "learn"
- "learning"
- "led-"
- "led."
- "led_"
- "leetcode"
- "les-"
- "les."
- "les_"
- "level"
- "leveldb"
- "lib-"
- "lib."
- "lib_"
- "librarie"
- "library"
- "license"
- "life"
- "liferay"
- "light"
- "lightbox"
- "like"
- "line"
- "link"
- "linked"
- "linkedin"
- "linux"
- "lisp"
- "list"
- "lite"
- "little"
- "load"
- "loader"
- "local"
- "location"
- "lock"
- "log-"
- "log."
- "log_"
- "logger"
- "logging"
- "logic"
- "login"
- "logstash"
- "longer"
- "look"
- "love"
- "lua-"
- "lua."
- "lua_"
- "mac-"
- "mac."
- "mac_"
- "machine"
- "made"
- "magento"
- "magic"
- "mail"
- "make"
- "maker"
- "making"
- "man-"
- "man."
- "man_"
- "manage"
- "manager"
- "manifest"
- "manual"
- "map-"
- "map-"
- "map."
- "map."
- "map_"
- "map_"
- "mapper"
- "mapping"
- "markdown"
- "markup"
- "master"
- "math"
- "matrix"
- "maven"
- "md5"
- "mean"
- "media"
- "mediawiki"
- "meetup"
- "memcached"
- "memory"
- "menu"
- "merchant"
- "message"
- "messaging"
- "meta"
- "metadata"
- "meteor"
- "method"
- "metric"
- "micro"
- "middleman"
- "migration"
- "minecraft"
- "miner"
- "mini"
- "minimal"
- "mirror"
- "mit-"
- "mit."
- "mit_"
- "mobile"
- "mocha"
- "mock"
- "mod-"
- "mod."
- "mod_"
- "mode"
- "model"
- "modern"
- "modular"
- "module"
- "modx"
- "money"
- "mongo"
- "mongodb"
- "mongoid"
- "mongoose"
- "monitor"
- "monkey"
- "more"
- "motion"
- "moved"
- "movie"
- "mozilla"
- "mqtt"
- "mule"
- "multi"
- "multiple"
- "music"
- "mustache"
- "mvc-"
- "mvc."
- "mvc_"
- "mysql"
- "nagio"
- "name"
- "native"
- "need"
- "neo-"
- "neo."
- "neo_"
- "nest"
- "nested"
- "net-"
- "net."
- "net_"
- "nette"
- "network"
- "new-"
- "new-"
- "new."
- "new."
- "new_"
- "new_"
- "next"
- "nginx"
- "ninja"
- "nlp-"
- "nlp."
- "nlp_"
- "node"
- "nodej"
- "nosql"
- "not-"
- "not."
- "not_"
- "note"
- "notebook"
- "notepad"
- "notice"
- "notifier"
- "now-"
- "now."
- "now_"
- "number"
- "oauth"
- "object"
- "objective"
- "obsolete"
- "ocaml"
- "octopres"
- "official"
- "old-"
- "old."
- "old_"
- "onboard"
- "online"
- "only"
- "open"
- "opencv"
- "opengl"
- "openshift"
- "openwrt"
- "option"
- "oracle"
- "org-"
- "org."
- "org_"
- "origin"
- "original"
- "orm-"
- "orm."
- "orm_"
- "osx-"
- "osx_"
- "our-"
- "our."
- "our_"
- "out-"
- "out."
- "out_"
- "output"
- "over"
- "overview"
- "own-"
- "own."
- "own_"
- "pack"
- "package"
- "packet"
- "page"
- "page"
- "panel"
- "paper"
- "paperclip"
- "para"
- "parallax"
- "parallel"
- "parse"
- "parser"
- "parsing"
- "particle"
- "party"
- "password"
- "patch"
- "path"
- "pattern"
- "payment"
- "paypal"
- "pdf-"
- "pdf."
- "pdf_"
- "pebble"
- "people"
- "perl"
- "personal"
- "phalcon"
- "phoenix"
- "phone"
- "phonegap"
- "photo"
- "php-"
- "php."
- "php_"
- "physic"
- "picker"
- "pipeline"
- "platform"
- "play"
- "player"
- "please"
- "plu-"
- "plu."
- "plu_"
- "plug-in"
- "plugin"
- "plupload"
- "png-"
- "png."
- "png_"
- "poker"
- "polyfill"
- "polymer"
- "pool"
- "pop-"
- "pop."
- "pop_"
- "popcorn"
- "popup"
- "port"
- "portable"
- "portal"
- "portfolio"
- "post"
- "power"
- "powered"
- "powerful"
- "prelude"
- "pretty"
- "preview"
- "principle"
- "print"
- "pro-"
- "pro."
- "pro_"
- "problem"
- "proc"
- "product"
- "profile"
- "profiler"
- "program"
- "progres"
- "project"
- "protocol"
- "prototype"
- "provider"
- "proxy"
- "public"
- "pull"
- "puppet"
- "pure"
- "purpose"
- "push"
- "pusher"
- "pyramid"
- "python"
- "quality"
- "query"
- "queue"
- "quick"
- "rabbitmq"
- "rack"
- "radio"
- "rail"
- "railscast"
- "random"
- "range"
- "raspberry"
- "rdf-"
- "rdf."
- "rdf_"
- "react"
- "reactive"
- "read"
- "reader"
- "readme"
- "ready"
- "real"
- "reality"
- "real-time"
- "realtime"
- "recipe"
- "recorder"
- "red-"
- "red."
- "red_"
- "reddit"
- "redi"
- "redmine"
- "reference"
- "refinery"
- "refresh"
- "registry"
- "related"
- "release"
- "remote"
- "rendering"
- "repo"
- "report"
- "request"
- "require"
- "required"
- "requirej"
- "research"
- "resource"
- "response"
- "resque"
- "rest"
- "restful"
- "resume"
- "reveal"
- "reverse"
- "review"
- "riak"
- "rich"
- "right"
- "ring"
- "robot"
- "role"
- "room"
- "router"
- "routing"
- "rpc-"
- "rpc."
- "rpc_"
- "rpg-"
- "rpg."
- "rpg_"
- "rspec"
- "ruby-"
- "ruby."
- "ruby_"
- "rule"
- "run-"
- "run."
- "run_"
- "runner"
- "running"
- "runtime"
- "rust"
- "rvm-"
- "rvm."
- "rvm_"
- "salt"
- "sample"
- "sample"
- "sandbox"
- "sas-"
- "sas."
- "sas_"
- "sbt-"
- "sbt."
- "sbt_"
- "scala"
- "scalable"
- "scanner"
- "schema"
- "scheme"
- "school"
- "science"
- "scraper"
- "scratch"
- "screen"
- "script"
- "scroll"
- "scs-"
- "scs."
- "scs_"
- "sdk-"
- "sdk."
- "sdk_"
- "sdl-"
- "sdl."
- "sdl_"
- "search"
- "secure"
- "security"
- "see-"
- "see."
- "see_"
- "seed"
- "select"
- "selector"
- "selenium"
- "semantic"
- "sencha"
- "send"
- "sentiment"
- "serie"
- "server"
- "service"
- "session"
- "set-"
- "set."
- "set_"
- "setting"
- "setting"
- "setup"
- "sha1"
- "sha2"
- "sha256"
- "share"
- "shared"
- "sharing"
- "sheet"
- "shell"
- "shield"
- "shipping"
- "shop"
- "shopify"
- "shortener"
- "should"
- "show"
- "showcase"
- "side"
- "silex"
- "simple"
- "simulator"
- "single"
- "site"
- "skeleton"
- "sketch"
- "skin"
- "slack"
- "slide"
- "slider"
- "slim"
- "small"
- "smart"
- "smtp"
- "snake"
- "snapshot"
- "snippet"
- "soap"
- "social"
- "socket"
- "software"
- "solarized"
- "solr"
- "solution"
- "solver"
- "some"
- "soon"
- "source"
- "space"
- "spark"
- "spatial"
- "spec"
- "sphinx"
- "spine"
- "spotify"
- "spree"
- "spring"
- "sprite"
- "sql-"
- "sql."
- "sql_"
- "sqlite"
- "ssh-"
- "ssh."
- "ssh_"
- "stack"
- "staging"
- "standard"
- "stanford"
- "start"
- "started"
- "starter"
- "startup"
- "stat"
- "statamic"
- "state"
- "static"
- "statistic"
- "statsd"
- "statu"
- "steam"
- "step"
- "still"
- "stm-"
- "stm."
- "stm_"
- "storage"
- "store"
- "storm"
- "story"
- "strategy"
- "stream"
- "streaming"
- "string"
- "stripe"
- "structure"
- "studio"
- "study"
- "stuff"
- "style"
- "sublime"
- "sugar"
- "suite"
- "summary"
- "super"
- "support"
- "supported"
- "svg-"
- "svg."
- "svg_"
- "svn-"
- "svn."
- "svn_"
- "swagger"
- "swift"
- "switch"
- "switcher"
- "symfony"
- "symphony"
- "sync"
- "synopsi"
- "syntax"
- "system"
- "system"
- "tab-"
- "tab-"
- "tab."
- "tab."
- "tab_"
- "tab_"
- "table"
- "tag-"
- "tag-"
- "tag."
- "tag."
- "tag_"
- "tag_"
- "talk"
- "target"
- "task"
- "tcp-"
- "tcp."
- "tcp_"
- "tdd-"
- "tdd."
- "tdd_"
- "team"
- "tech"
- "template"
- "term"
- "terminal"
- "testing"
- "tetri"
- "text"
- "textmate"
- "theme"
- "theory"
- "three"
- "thrift"
- "time"
- "timeline"
- "timer"
- "tiny"
- "tinymce"
- "tip-"
- "tip."
- "tip_"
- "title"
- "todo"
- "todomvc"
- "token"
- "tool"
- "toolbox"
- "toolkit"
- "top-"
- "top."
- "top_"
- "tornado"
- "touch"
- "tower"
- "tracker"
- "tracking"
- "traffic"
- "training"
- "transfer"
- "translate"
- "transport"
- "tree"
- "trello"
- "try-"
- "try."
- "try_"
- "tumblr"
- "tut-"
- "tut."
- "tut_"
- "tutorial"
- "tweet"
- "twig"
- "twitter"
- "type"
- "typo"
- "ubuntu"
- "uiview"
- "ultimate"
- "under"
- "unit"
- "unity"
- "universal"
- "unix"
- "update"
- "updated"
- "upgrade"
- "upload"
- "uploader"
- "uri-"
- "uri."
- "uri_"
- "url-"
- "url."
- "url_"
- "usage"
- "usb-"
- "usb."
- "usb_"
- "use-"
- "use."
- "use_"
- "used"
- "useful"
- "user"
- "using"
- "util"
- "utilitie"
- "utility"
- "vagrant"
- "validator"
- "value"
- "variou"
- "varnish"
- "version"
- "via-"
- "via."
- "via_"
- "video"
- "view"
- "viewer"
- "vim-"
- "vim."
- "vim_"
- "vimrc"
- "virtual"
- "vision"
- "visual"
- "vpn"
- "want"
- "warning"
- "watch"
- "watcher"
- "wave"
- "way-"
- "way."
- "way_"
- "weather"
- "web-"
- "web_"
- "webapp"
- "webgl"
- "webhook"
- "webkit"
- "webrtc"
- "website"
- "websocket"
- "welcome"
- "welcome"
- "what"
- "what'"
- "when"
- "where"
- "which"
- "why-"
- "why."
- "why_"
- "widget"
- "wifi"
- "wiki"
- "win-"
- "win."
- "win_"
- "window"
- "wip-"
- "wip."
- "wip_"
- "within"
- "without"
- "wizard"
- "word"
- "wordpres"
- "work"
- "worker"
- "workflow"
- "working"
- "workshop"
- "world"
- "wrapper"
- "write"
- "writer"
- "writing"
- "written"
- "www-"
- "www."
- "www_"
- "xamarin"
- "xcode"
- "xml-"
- "xml."
- "xml_"
- "xmpp"
- "xxxxxx"
- "yahoo"
- "yaml"
- "yandex"
- "yeoman"
- "yet-"
- "yet."
- "yet_"
- "yii-"
- "yii."
- "yii_"
- "youtube"
- "yui-"
- "yui."
- "yui_"
- "zend"
- "zero"
- "zip-"
- "zip."
- "zip_"
- "zsh-"
- "zsh."
- "zsh_"
================================================
FILE: go.mod
================================================
module github.com/trufflesecurity/trufflehog/v3
go 1.24.0
toolchain go1.24.5
replace github.com/jpillora/overseer => github.com/trufflesecurity/overseer v1.2.8
// Coinbase archived this library and it has some vulnerable dependencies so we've forked.
replace github.com/coinbase/waas-client-library-go => github.com/trufflesecurity/waas-client-library-go v1.0.9
require (
cloud.google.com/go/secretmanager v1.16.0
cloud.google.com/go/storage v1.56.1
github.com/BobuSumisu/aho-corasick v1.0.3
github.com/TheZeroSlave/zapsentry v1.23.0
github.com/adrg/strutil v0.3.1
github.com/alecthomas/kingpin/v2 v2.4.0
github.com/avast/apkparser v0.0.0-20250626104540-d53391f4d69d
github.com/aws/aws-sdk-go-v2 v1.39.0
github.com/aws/aws-sdk-go-v2/config v1.31.7
github.com/aws/aws-sdk-go-v2/credentials v1.18.11
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.5
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.0
github.com/aws/aws-sdk-go-v2/service/sns v1.38.2
github.com/aws/aws-sdk-go-v2/service/sts v1.38.3
github.com/aws/smithy-go v1.23.0
github.com/aymanbagabas/go-osc52 v1.2.1
github.com/bill-rich/go-syslog v0.0.0-20220413021637-49edb52a574c
github.com/bradleyfalzon/ghinstallation/v2 v2.16.0
github.com/brianvoe/gofakeit/v7 v7.6.0
github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v1.3.6
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/couchbase/gocb/v2 v2.11.0
github.com/crewjam/rfc5424 v0.1.0
github.com/csnewman/dextk v0.3.0
github.com/docker/docker v28.3.3+incompatible
github.com/dustin/go-humanize v1.0.1
github.com/elastic/go-elasticsearch/v8 v8.17.1
github.com/envoyproxy/protoc-gen-validate v1.2.1
github.com/fatih/color v1.18.0
github.com/felixge/fgprof v0.9.5
github.com/gabriel-vasile/mimetype v1.4.10
github.com/getsentry/sentry-go v0.32.0
github.com/go-errors/errors v1.5.1
github.com/go-git/go-git/v5 v5.13.2
github.com/go-logr/logr v1.4.3
github.com/go-logr/zapr v1.3.0
github.com/go-redis/redis v6.15.9+incompatible
github.com/go-sql-driver/mysql v1.8.1
github.com/gobwas/glob v0.2.3
github.com/golang-jwt/jwt/v5 v5.2.3
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.20.6
github.com/google/go-github/v67 v67.0.0
github.com/google/uuid v1.6.0
github.com/googleapis/gax-go/v2 v2.16.0
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/jedib0t/go-pretty/v6 v6.6.8
github.com/jlaffaye/ftp v0.2.0
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/pgzip v1.2.6
github.com/kylelemons/godebug v1.1.0
github.com/lestrrat-go/jwx/v3 v3.0.12
github.com/lib/pq v1.10.9
github.com/lrstanley/bubblezone v0.0.0-20250404061050-e13639e27357
github.com/mariduv/ldap-verify v0.0.2
github.com/marusama/semaphore/v2 v2.5.0
github.com/mattn/go-isatty v0.0.20
github.com/mholt/archives v0.0.0-20241216060121-23e0af8fe73d
github.com/microsoft/go-mssqldb v1.8.2
github.com/mitchellh/go-ps v1.0.0
github.com/muesli/reflow v0.3.0
github.com/patrickmn/go-cache v2.1.0+incompatible
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/repeale/fp-go v0.11.1
github.com/sassoftware/go-rpmutils v0.4.0
github.com/schollz/progressbar/v3 v3.17.1
github.com/sendgrid/sendgrid-go v3.16.1+incompatible
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
github.com/shuheiktgw/go-travis v0.3.1
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go v0.34.0
github.com/testcontainers/testcontainers-go/modules/elasticsearch v0.34.0
github.com/testcontainers/testcontainers-go/modules/mongodb v0.34.0
github.com/testcontainers/testcontainers-go/modules/mssql v0.34.0
github.com/testcontainers/testcontainers-go/modules/mysql v0.34.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.34.0
github.com/trufflesecurity/disk-buffer-reader v0.2.1
github.com/wasilibs/go-re2 v1.9.0
github.com/xo/dburl v0.23.8
gitlab.com/gitlab-org/api/client-go v1.12.0
go.mongodb.org/mongo-driver v1.17.4
go.uber.org/automaxprocs v1.6.0
go.uber.org/mock v0.6.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.46.0
golang.org/x/net v0.48.0
golang.org/x/oauth2 v0.34.0
golang.org/x/sync v0.19.0
golang.org/x/text v0.32.0
golang.org/x/time v0.14.0
google.golang.org/api v0.259.0
google.golang.org/protobuf v1.36.11
gopkg.in/h2non/gock.v1 v1.1.2
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
pault.ag/go/debian v0.18.0
pgregory.net/rapid v1.1.0
sigs.k8s.io/yaml v1.4.0
)
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go v0.121.6 // indirect
cloud.google.com/go/auth v0.18.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
dario.cat/mergo v1.0.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect
github.com/DataDog/zstd v1.5.5 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.5 // indirect
github.com/STARRY-S/zip v0.2.1 // indirect
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.2 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.3 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.0 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/couchbase/gocbcore/v10 v10.8.0 // indirect
github.com/couchbase/gocbcoreps v0.1.3 // indirect
github.com/couchbase/goprotostellar v1.0.2 // indirect
github.com/couchbaselabs/gocbconnstr/v2 v2.0.0-20240607131231-fb385523de28 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/docker/cli v28.2.2+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/elastic/elastic-transport-go/v8 v8.6.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/go-github/v72 v72.0.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
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.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nwaples/rardecode/v2 v2.2.1 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/sorairolake/lzip-go v0.3.5 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/trufflesecurity/touchfile v0.1.1 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/vbatts/tar-split v0.12.1 // indirect
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/term v0.38.0 // indirect
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
google.golang.org/grpc v1.78.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
pault.ag/go/topsort v0.1.1 // indirect
)
================================================
FILE: go.sum
================================================
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/secretmanager v1.16.0 h1:19QT7ZsLJ8FSP1k+4esQvuCD7npMJml6hYzilxVyT+k=
cloud.google.com/go/secretmanager v1.16.0/go.mod h1://C/e4I8D26SDTz1f3TQcddhcmiC3rMEl0S1Cakvs3Q=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.56.1 h1:n6gy+yLnHn0hTwBFzNn8zJ1kqWfR91wzdM8hjRF4wP0=
cloud.google.com/go/storage v1.56.1/go.mod h1:C9xuCZgFl3buo2HZU/1FncgvvOgTAs/rnh4gF4lMg0s=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8a+4nPE9g=
github.com/BobuSumisu/aho-corasick v1.0.3/go.mod h1:hm4jLcvZKI2vRF2WDU1N4p/jpWtpOzp3nLmi9AzX/XE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
github.com/TheZeroSlave/zapsentry v1.23.0 h1:TKyzfEL7LRlRr+7AvkukVLZ+jZPC++ebCUv7ZJHl1AU=
github.com/TheZeroSlave/zapsentry v1.23.0/go.mod h1:3DRFLu4gIpnCTD4V9HMCBSaqYP8gYU7mZickrs2/rIY=
github.com/adrg/strutil v0.3.1 h1:OLvSS7CSJO8lBii4YmBt8jiK9QOtB9CzCzwl4Ic/Fz4=
github.com/adrg/strutil v0.3.1/go.mod h1:8h90y18QLrs11IBffcGX3NW/GFBXCMcNg4M7H6MspPA=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/avast/apkparser v0.0.0-20250626104540-d53391f4d69d h1:PGSn2pnK/u5ZBompy83R6Wo4BqLYp3dX43QWDoPv7TA=
github.com/avast/apkparser v0.0.0-20250626104540-d53391f4d69d/go.mod h1:3F9A8btIerUcuy7Fmno+g/nIk4ELKJ6NCs2/KK1bvLs=
github.com/aws/aws-sdk-go-v2 v1.39.0 h1:xm5WV/2L4emMRmMjHFykqiA4M/ra0DJVSWUkDyBjbg4=
github.com/aws/aws-sdk-go-v2 v1.39.0/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
github.com/aws/aws-sdk-go-v2/config v1.31.7 h1:zS1O6hr6t0nZdBCMFc/c9OyZFyLhXhf/B2IZ9Y0lRQE=
github.com/aws/aws-sdk-go-v2/config v1.31.7/go.mod h1:GpHmi1PQDdL5pP4JaB00pU0ek4EXVcYH7IkjkUadQmM=
github.com/aws/aws-sdk-go-v2/credentials v1.18.11 h1:1Fnb+7Dk96/VYx/uYfzk5sU2V0b0y2RWZROiMZCN/Io=
github.com/aws/aws-sdk-go-v2/credentials v1.18.11/go.mod h1:iuvn9v10dkxU4sDgtTXGWY0MrtkEcmkUmjv4clxhuTc=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 h1:Is2tPmieqGS2edBnmOJIbdvOA6Op+rRpaYR60iBAwXM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7/go.mod h1:F1i5V5421EGci570yABvpIXgRIBPb5JM+lSkHF6Dq5w=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.5 h1:fSuJX/VBJKufwJG/szWgUdRJVyRiEQDDXNh/6NPrTBg=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.5/go.mod h1:LvN0noQuST+3Su55Wl++BkITpptnfN9g6Ohkv4zs9To=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 h1:UCxq0X9O3xrlENdKf1r9eRJoKz/b0AfGkpp3a7FPlhg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7/go.mod h1:rHRoJUNUASj5Z/0eqI4w32vKvC7atoWR0jC+IkmVH8k=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 h1:Y6DTZUn7ZUC4th9FMBbo8LVE+1fyq3ofw+tRwkUd3PY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7/go.mod h1:x3XE6vMnU9QvHN/Wrx2s44kwzV2o2g5x/siw4ZUJ9g8=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.7 h1:BszAktdUo2xlzmYHjWMq70DqJ7cROM8iBd3f6hrpuMQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.7/go.mod h1:XJ1yHki/P7ZPuG4fd3f0Pg/dSGA2cTQBCLw82MH2H48=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.7 h1:zmZ8qvtE9chfhBPuKB2aQFxW5F/rpwXUgmcVCgQzqRw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.7/go.mod h1:vVYfbpd2l+pKqlSIDIOgouxNsGu5il9uDp0ooWb0jys=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 h1:mLgc5QIgOy26qyh5bvW+nDoAppxgn3J2WV3m9ewq7+8=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7/go.mod h1:wXb/eQnqt8mDQIQTTmcw58B5mYGxzLGZGK8PWNFZ0BA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.7 h1:u3VbDKUCWarWiU+aIUK4gjTr/wQFXV17y3hgNno9fcA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.7/go.mod h1:/OuMQwhSyRapYxq6ZNpPer8juGNrB4P5Oz8bZ2cgjQE=
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.0 h1:k5JXPr+2SrPDwM3PdygZUenn0lVPLa3KOs7cCYqinFs=
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.0/go.mod h1:xajPTguLoeQMAOE44AAP2RQoUhF8ey1g5IFHARv71po=
github.com/aws/aws-sdk-go-v2/service/sns v1.38.2 h1:Djc2m7mTPuizL1iMxJfMc209PDy2AqiN1AXrtq/rBdY=
github.com/aws/aws-sdk-go-v2/service/sns v1.38.2/go.mod h1:kHMCS+JDWKuKSDP9J/v3dlV2S9zNBKbXzaLy/kHSdEE=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.2 h1:rcoTaYOhGE/zfxE1uR6X5fvj+uKkqeCNRE0rBbiQM34=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.2/go.mod h1:Ql6jE9kyyWI5JHn+61UT/Y5Z0oyVJGmgmJbZD5g4unY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.3 h1:BSIfeFtU9tlSt8vEYS7KzurMoAuYzYPWhcZiMtxVf2M=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.3/go.mod h1:XclEty74bsGBCr1s0VSaA11hQ4ZidK4viWK7rRfO88I=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.3 h1:yEiZ0ztgji2GsCb/6uQSITXcGdtmWMfLRys0jJFiUkc=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.3/go.mod h1:Z+Gd23v97pX9zK97+tX4ppAgqCt3Z2dIXB02CtBncK8=
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/aymanbagabas/go-osc52 v1.2.1 h1:q2sWUyDcozPLcLabEMd+a+7Ea2DitxZVN9hTxab9L4E=
github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bill-rich/go-syslog v0.0.0-20220413021637-49edb52a574c h1:tSME5FDS02qQll3JYodI6RZR/g4EKOHApGv1wMZT+Z0=
github.com/bill-rich/go-syslog v0.0.0-20220413021637-49edb52a574c/go.mod h1:+sCc6hztur+oZCLOsNk6wCCy+GLrnSNHSRmTnnL+8iQ=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A=
github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/bradleyfalzon/ghinstallation/v2 v2.16.0 h1:B91r9bHtXp/+XRgS5aZm6ZzTdz3ahgJYmkt4xZkgDz8=
github.com/bradleyfalzon/ghinstallation/v2 v2.16.0/go.mod h1:OeVe5ggFzoBnmgitZe/A+BqGOnv1DvU/0uiLQi1wutM=
github.com/brianvoe/gofakeit/v7 v7.6.0 h1:M3RUb5CuS2IZmF/cP+O+NdLxJEuDAZxNQBwPbbqR6h4=
github.com/brianvoe/gofakeit/v7 v7.6.0/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0=
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
github.com/couchbase/gocb/v2 v2.11.0 h1:OVB+KlVeXlKVtziKx/LWZT7DClLsoQHQFrI4wan5Ijc=
github.com/couchbase/gocb/v2 v2.11.0/go.mod h1:Y+lODSgyVzDSaf0Sy8sIzIa0RTAw3vlZUsjY6+FUq9Y=
github.com/couchbase/gocbcore/v10 v10.8.0 h1:zDcJyYqOirFyC8T/aVvNL4N9oj6GI4qtaBuTGGWCDb4=
github.com/couchbase/gocbcore/v10 v10.8.0/go.mod h1:OWKfU9R5Nm5V3QZBtfdZl5qCfgxtxTqOgXiNr4pn9/c=
github.com/couchbase/gocbcoreps v0.1.3 h1:fILaKGCjxFIeCgAUG8FGmRDSpdrRggohOMKEgO9CUpg=
github.com/couchbase/gocbcoreps v0.1.3/go.mod h1:hBFpDNPnRno6HH5cRXExhqXYRmTsFJlFHQx7vztcXPk=
github.com/couchbase/goprotostellar v1.0.2 h1:yoPbAL9sCtcyZ5e/DcU5PRMOEFaJrF9awXYu3VPfGls=
github.com/couchbase/goprotostellar v1.0.2/go.mod h1:5/yqVnZlW2/NSbAWu1hPJCFBEwjxgpe0PFFOlRixnp4=
github.com/couchbaselabs/gocaves/client v0.0.0-20250107114554-f96479220ae8 h1:MQfvw4BiLTuyR69FuA5Kex+tXUeLkH+/ucJfVL1/hkM=
github.com/couchbaselabs/gocaves/client v0.0.0-20250107114554-f96479220ae8/go.mod h1:AVekAZwIY2stsJOMWLAS/0uA/+qdp7pjO8EHnl61QkY=
github.com/couchbaselabs/gocbconnstr/v2 v2.0.0-20240607131231-fb385523de28 h1:lhGOw8rNG6RAadmmaJAF3PJ7MNt7rFuWG7BHCYMgnGE=
github.com/couchbaselabs/gocbconnstr/v2 v2.0.0-20240607131231-fb385523de28/go.mod h1:o7T431UOfFVHDNvMBUmUxpHnhivwv7BziUao/nMl81E=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/crewjam/rfc5424 v0.1.0 h1:MSeXJm22oKovLzWj44AHwaItjIMUMugYGkEzfa831H8=
github.com/crewjam/rfc5424 v0.1.0/go.mod h1:RCi9M3xHVOeerf6ULZzqv2xOGRO/zYaVUeRyPnBW3gQ=
github.com/csnewman/dextk v0.3.0 h1:gigNZlZRNfCuARV7depunRlafEAzGhyvgBQo1FT3/0M=
github.com/csnewman/dextk v0.3.0/go.mod h1:FcDoI3258ea0KPQogyv4iazQRGcLFNOW+I4pHBUfNO0=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A=
github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elastic/elastic-transport-go/v8 v8.6.1 h1:h2jQRqH6eLGiBSN4eZbQnJLtL4bC5b4lfVFRjw2R4e4=
github.com/elastic/elastic-transport-go/v8 v8.6.1/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk=
github.com/elastic/go-elasticsearch/v8 v8.17.1 h1:bOXChDoCMB4TIwwGqKd031U8OXssmWLT3UrAr9EGs3Q=
github.com/elastic/go-elasticsearch/v8 v8.17.1/go.mod h1:MVJCtL+gJJ7x5jFeUmA20O7rvipX8GcQmo5iBcmaJn4=
github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM=
github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM=
github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs=
github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo=
github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY=
github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
github.com/google/go-github/v67 v67.0.0 h1:g11NDAmfaBaCO8qYdI9fsmbaRipHNWRIU/2YGvlh4rg=
github.com/google/go-github/v67 v67.0.0/go.mod h1:zH3K7BxjFndr9QSeFibx4lTKkYS3K9nDanoI1NjaOtY=
github.com/google/go-github/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi655PPvBM=
github.com/google/go-github/v72 v72.0.0/go.mod h1:WWtw8GMRiL62mvIquf1kO3onRHeWWKmK01qdCY8c5fg=
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8=
github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc=
github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/s3 v1.1.4 h1:YCCKDWzb/Ye9EBNd83ATRF/8wPEy0xd43Rezb6u6fzc=
github.com/jpillora/s3 v1.1.4/go.mod h1:yedE603V+crlFi1Kl/5vZJaBu9pUzE9wvKegU/lF2zs=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d h1:RnWZeH8N8KXfbwMTex/KKMYMj0FJRCF6tQubUuQ02GM=
github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d/go.mod h1:phT/jsRPBAEqjAibu1BurrabCBNTYiVI+zbmyCZJY6Q=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg=
github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lrstanley/bubblezone v0.0.0-20250404061050-e13639e27357 h1:DxFncLGTrDh5v0z+DE7h+qjD5tPCGR+3knGVVfT3YLI=
github.com/lrstanley/bubblezone v0.0.0-20250404061050-e13639e27357/go.mod h1:S5etECMx+sZnW0Gm100Ma9J1PgVCTgNyFaqGu2b08b4=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mariduv/ldap-verify v0.0.2 h1:NBdDTYyWDr71CONVcizasqL/AA9tQ2RNgLhTgnyfquI=
github.com/mariduv/ldap-verify v0.0.2/go.mod h1:d/7+kkMBGDs9LPZ/7hmduYqtOkRIJcgpa8dL+9CsveE=
github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM=
github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mholt/archives v0.0.0-20241216060121-23e0af8fe73d h1:Vw3f39TqFSQLA+OyW+8SouppHTYzX8/fDv6Ao8uj3Ho=
github.com/mholt/archives v0.0.0-20241216060121-23e0af8fe73d/go.mod h1:j/Ire/jm42GN7h90F5kzj6hf6ZFzEH66de+hmjEKu+I=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I=
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nwaples/rardecode/v2 v2.2.1 h1:DgHK/O/fkTQEKBJxBMC5d9IU8IgauifbpG78+rZJMnI=
github.com/nwaples/rardecode/v2 v2.2.1/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/paulbellamy/ratecounter v0.2.0 h1:2L/RhJq+HA8gBQImDXtLPrDXK5qAj6ozWVK/zFXVJGs=
github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
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/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=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg=
github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI=
github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U=
github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1GjJohAA0p6hVEaDtHWWs=
github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shuheiktgw/go-travis v0.3.1 h1:SAT16mi77ccqogOslnXxBXzXbpeyChaIYUwi2aJpVZY=
github.com/shuheiktgw/go-travis v0.3.1/go.mod h1:avnFFDqJDdRHwlF9tgqvYi3asQCm/HGL8aLxYiKa4Yg=
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkvclm+pWm1Lk4YrREb4IOIb/YdFO0p2M=
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
github.com/smartystreets/gunit v1.1.3 h1:32x+htJCu3aMswhPw3teoJ+PnWPONqdNgaGs6Qt8ZaU=
github.com/smartystreets/gunit v1.1.3/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg=
github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo=
github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ=
github.com/testcontainers/testcontainers-go/modules/elasticsearch v0.34.0 h1:BBwJUs9xBpt1uOfO+yAr2pYW75MsyzuO/o70HTPnhe4=
github.com/testcontainers/testcontainers-go/modules/elasticsearch v0.34.0/go.mod h1:OqhRGYR+5VG0Dw506F6Ho9I4YG1kB+o9uPTKC0uPUA8=
github.com/testcontainers/testcontainers-go/modules/mongodb v0.34.0 h1:o3bgcECyBFfMwqexCH/6vIJ8XzbCffCP/Euesu33rgY=
github.com/testcontainers/testcontainers-go/modules/mongodb v0.34.0/go.mod h1:ljLR42dN7k40CX0dp30R8BRIB3OOdvr7rBANEpfmMs4=
github.com/testcontainers/testcontainers-go/modules/mssql v0.34.0 h1:4Pf7ILuLnxhpeTgQfKzEMPuMQhasU3VaYer9l5HrQ3s=
github.com/testcontainers/testcontainers-go/modules/mssql v0.34.0/go.mod h1:L2eMWZ49X0XjewabzJ6TXSY5z4SAWM/2WOBqlIxYFD8=
github.com/testcontainers/testcontainers-go/modules/mysql v0.34.0 h1:Tqz17mGXjPORHFS/oBUGdeJyIsZXLsVVHRhaBqhewGI=
github.com/testcontainers/testcontainers-go/modules/mysql v0.34.0/go.mod h1:hDpm3DLfjo7rd6232wWflEBDGr6Ow9ys43mJTiJwWx8=
github.com/testcontainers/testcontainers-go/modules/postgres v0.34.0 h1:c51aBXT3v2HEBVarmaBnsKzvgZjC5amn0qsj8Naqi50=
github.com/testcontainers/testcontainers-go/modules/postgres v0.34.0/go.mod h1:EWP75ogLQU4M4L8U+20mFipjV4WIR9WtlMXSB6/wiuc=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/trufflesecurity/disk-buffer-reader v0.2.1 h1:K9nNpX3xeWT2E6YRjlcc1X5c1NjgV9JS5T9aw2FjA8Q=
github.com/trufflesecurity/disk-buffer-reader v0.2.1/go.mod h1:uYwTCdxzV0o+qaeBMxflOsq4eu2WjrE46qGR2e80O9Y=
github.com/trufflesecurity/overseer v1.2.8 h1:VXlWPiwYaQmwNxY2W1rVulEAG9O6iF1S0LX3wionWYM=
github.com/trufflesecurity/overseer v1.2.8/go.mod h1:Dt6Y9LFpM+C/3rRWpy4//4iS5qrbb0pL3XvZqMd4zhg=
github.com/trufflesecurity/touchfile v0.1.1 h1:Snhd5VEa8Cxd+D60nvLEj2kVeb1omY2tWwnhDhjTqdo=
github.com/trufflesecurity/touchfile v0.1.1/go.mod h1:Yg/AUMrxAk+dWDUjIig0OyGgFOHFuWNw+t2S/GvO6Mk=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/wasilibs/go-re2 v1.9.0 h1:kjAd8qbNvV4Ve2Uf+zrpTCrDHtqH4dlsRXktywo73JQ=
github.com/wasilibs/go-re2 v1.9.0/go.mod h1:0sRtscWgpUdNA137bmr1IUgrRX0Su4dcn9AEe61y+yI=
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4=
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xo/dburl v0.23.8 h1:NwFghJfjaUW7tp+WE5mTLQQCfgseRsvgXjlSvk7x4t4=
github.com/xo/dburl v0.23.8/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
gitlab.com/gitlab-org/api/client-go v1.12.0 h1:vYeraq+4eC/5Scir5nWve3LPxLoY5rgW2qRNUgjKS2k=
gitlab.com/gitlab-org/api/client-go v1.12.0/go.mod h1:adtVJ4zSTEJ2fP5Pb1zF4Ox1OKFg0MH43yxpb0T0248=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs=
go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ=
google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
pault.ag/go/debian v0.18.0 h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ=
pault.ag/go/debian v0.18.0/go.mod h1:JFl0XWRCv9hWBrB5MDDZjA5GSEs1X3zcFK/9kCNIUmE=
pault.ag/go/topsort v0.1.1 h1:L0QnhUly6LmTv0e3DEzbN2q6/FGgAcQvaEw65S53Bg4=
pault.ag/go/topsort v0.1.1/go.mod h1:r1kc/L0/FZ3HhjezBIPaNVhkqv8L0UJ9bxRuHRVZ0q4=
pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw=
pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
================================================
FILE: hack/Dockerfile.protos
================================================
# trufflesecurity/protos:1.23
FROM golang:1.24-bullseye
ARG TARGETARCH
ARG TARGETOS
ENV PROTOC_VER=25.3
ENV PROTOC_GEN_GO_VER=v1.5.4
ENV PROTOC_GEN_VALIDATE_VER=v1.0.4
ENV GORELEASER_VERSION=1.19.2
RUN echo "building $TARGETARCH"
RUN go install github.com/dustin-decker/quill/cmd/quill@v0.5.1
RUN apt-get update; apt-get install apt-transport-https ca-certificates curl gnupg lsb-release -y; \
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg; \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian \
$(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null; \
apt-get update; apt-get install -y --no-install-recommends python3-pip docker-ce-cli docker-buildx-plugin docker-compose-plugin \
git netbase wget upx unzip && rm -rf /var/lib/apt/lists/*
RUN pip3 install --upgrade setuptools pip
RUN set -e; \
arch=$(echo $TARGETARCH | sed -e s/amd64/x86_64/ -e s/arm64/aarch_64/); \
wget -q https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VER}/protoc-${PROTOC_VER}-${TARGETOS}-${arch}.zip && unzip protoc-${PROTOC_VER}-${TARGETOS}-${arch}.zip -d /usr/local
RUN curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -; echo "deb https://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list; apt-get update; apt-get install -y google-cloud-cli
RUN echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | tee /etc/apt/sources.list.d/goreleaser.list; \
apt-get update; apt-get install -y goreleaser=${GORELEASER_VERSION} && rm -rf /var/lib/apt/lists/*
RUN go install "github.com/golang/protobuf/protoc-gen-go@${PROTOC_GEN_GO_VER}"
RUN go install gotest.tools/gotestsum@latest
RUN git clone https://github.com/envoyproxy/protoc-gen-validate $GOPATH/src/github.com/envoyproxy/protoc-gen-validate && \
cd $GOPATH/src/github.com/envoyproxy/protoc-gen-validate && \
git checkout ${PROTOC_GEN_VALIDATE_VER} && \
ln -s /usr/local/protoc/include/google google && \
make build
CMD ["bash"]
================================================
FILE: hack/bench/plot.gp
================================================
set terminal png size 800,600
set output "hack/bench/versions.png"
set title "User Time vs. Version"
set xlabel "Version"
set ylabel "Average User Time (s)"
set xtics rotate by -45
plot "hack/bench/plot.txt" using 2:xtic(1) with linespoints linestyle 1 notitle
================================================
FILE: hack/bench/plot.sh
================================================
#!/bin/bash
if [ $# -ne 2 ]; then
echo "Usage: $0 "
exit 1
fi
# Get the number of versions back to test from command line argument
num_versions="$2"
test_repo="$1"
bash hack/bench/versions.sh $test_repo $num_versions | tee hack/bench/plot.txt
gnuplot hack/bench/plot.gp
================================================
FILE: hack/bench/plot.txt
================================================
v3.33.0: 1.402
v3.32.2: 1.298
v3.32.1: 1.332
v3.32.0: 1.348
v3.31.6: 2.470
v3.31.5: 2.462
v3.31.4: 2.460
v3.31.3: 2.418
v3.31.2: 1.384
v3.31.1: 1.344
v3.31.0: 1.354
v3.30.0: 1.392
v3.29.1: 1.382
v3.29.0: 1.340
v3.28.7: 1.380
v3.28.6: 1.308
v3.28.5: 2.596
v3.28.4: 2.554
v3.28.3: 2.582
v3.28.1: 2.578
v3.28.2: 2.566
v3.28.0: 2.552
v3.27.1: 2.574
v3.26.0: 2.538
================================================
FILE: hack/bench/versions.sh
================================================
#!/bin/bash
if [ $# -ne 2 ]; then
echo "Usage: $0 "
exit 1
fi
# Get the number of versions back to test from command line argument
num_versions="$2"
test_repo="$1"
num_iterations=5
# Create a temporary folder to clone the repository
repo_tmp=$(mktemp -d)
# Set up a trap to remove the temporary folder on exit or failure
trap "rm -rf $repo_tmp" EXIT
# Clone the test repository to a temporary folder
git clone --quiet "$test_repo" $repo_tmp
# Get list of git tags, sorted from newest to oldest
tags=$(echo $(git describe --tags --always --dirty --match='v*') $(git tag --sort=-creatordate))
# Counter to keep track of number of tags checked out
count=0
# Loop over tags and checkout each one in turn, up to the specified number of versions
for tag in $tags
do
if [[ $count -eq $num_versions ]]; then
break
fi
# Skip RC tags
if [[ $tag == *"rc"* ]]; then
continue
fi
# Skip alpha tags
if [[ $tag == *"alpha"* ]]; then
continue
fi
# Use git checkout with the quiet flag to suppress output
git checkout $tag --quiet
# Run make install with suppressed output
make install > /dev/null
# Initialize the variable to store the sum of user times
user_time_sum=0
# Run each iteration 5 times and calculate the average user time
for i in {1..$num_iterations}
do
# Run trufflehog with suppressed output and capture user time with /usr/bin/time
tmpfile=$(mktemp)
/usr/bin/time -o $tmpfile trufflehog git "file://$repo_tmp" --no-verification --no-update >/dev/null 2>&1
time_output=$(cat $tmpfile)
rm $tmpfile
# Extract the user time from the output
user_time=$(echo $time_output | awk '{print $3}')
# Add the user time to the sum
user_time_sum=$(echo "$user_time_sum + $user_time" | bc)
done
# Calculate the average user time
average_user_time=$(echo "scale=3; $user_time_sum / $num_iterations" | bc)
# Print the average user time output for this iteration in the specified format
echo "$tag: $average_user_time"
# Increment the counter
count=$((count+1))
done
================================================
FILE: hack/docs/Adding_Detectors_Internal.md
================================================
# Secret Detectors
Secret Detectors have these two major functions:
1. Given some bytes, extract possible secrets, typically using a regex.
2. Validate the secrets against the target API, typically using a HTTP client.
The purpose of Secret Detectors is to discover secrets with exceptionally high signal. High rates of false positives are not accepted.
## Table of Contents
- [Secret Detectors](#secret-detectors)
- [Table of Contents](#table-of-contents)
- [Getting Started](#getting-started)
- [Sourcing Guidelines](#sourcing-guidelines)
- [Development Guidelines](#development-guidelines)
- [Development Dependencies](#development-dependencies)
- [Creating a new Secret Scanner](#creating-a-new-secret-detector)
- [Addendum](#addendum)
- [Managing Test Secrets](#managing-test-secrets)
- [Setting up Google Cloud SDK](#setting-up-google-cloud-sdk)
## Getting Started
### Sourcing Guidelines
We are interested in detectors for services that meet at least one of these criteria
- host data (they store any sort of data provided)
- have paid services (having a free or trial tier is okay though)
If you think that something should be included outside of these guidelines, please let us know.
### Development Guidelines
- When reasonable, favor using the `net/http` library to make requests instead of bringing in another library.
- Use the [`common.SaneHttpClient`](pkg/common/http.go) for the `http.Client` whenever possible.
- We recommend an editor with gopls integration (such as Vscode with Go plugin) for benefits like easily running tests, autocompletion, linting, type checking, etc.
### Development Dependencies
- A GitLab account
- A Google account
- [Google Cloud SDK installed](#setting-up-google-cloud-sdk)
- Go 1.17+
- Make
### Adding New Token Formats to an Existing Scanner
In some instances, services will update their token format, requiring a new regex to properly detect secrets in addition to supporting the previous token format. Accommodating this can be done without adding a net-new detector. [We provide a `Versioner` interface](https://github.com/trufflesecurity/trufflehog/blob/e18cfd5e0af1469a9f05b8d5732bcc94c39da49c/pkg/detectors/detectors.go#L30) that can be implemented.
1. Create two new directories `v1` and `v2`. Move the existing detector and tests into `v1`, and add new files to `v2`.
Ex: `/` -> `/v1/`, `/v2/`
Note: Be sure to update the tests to reference the new secret values in GSM, or the tests will fail.
2. Implement the `Versioner` interface. [GitHub example implementation.](/pkg/detectors/github/v1/github_old.go#L23)
3. Add a 'version' field in ExtraData for both existing and new detector versions.
4. Update the existing detector in DefaultDetectors in `/pkg/engine/defaults/defaults.go`
5. Proceed from step 3 of [Creating a new Secret Scanner](#creating-a-new-secret-scanner)
### Creating a new Secret Scanner
1. Identify the Secret Detector name from the [/proto/detectors.proto](/proto/detectors.proto) `DetectorType` enum.
2. Generate the Secret Detector
```bash
go run hack/generate/generate.go detector
```
3. Complete the secret detector.
The previous step templated a boilerplate + some example code as a package in the `pkg/detectors` folder for you to work on.
The secret detector can be completed with these general steps:
1. Add the test secret to GCP Secrets. See [managing test secrets](#managing-test-secrets)
2. Update the pattern regex and keywords. Try iterating with [regex101.com](http://regex101.com/).
3. Update the verifier code to use a non-destructive API call that can determine whether the secret is valid or not.
* Make sure you understand [verification indeterminacy](#verification-indeterminacy).
4. Update the tests with these test cases at minimum:
1. Found and verified (using a credential loaded from GCP Secrets)
2. Found and unverified (determinately, i.e. the secret is invalid)
3. Found and unverified (indeterminately due to timeout)
4. Found and unverified (indeterminately due to an unexpected API response)
5. Not found
6. Any false positive cases that you come across
5. Add your new detector to DefaultDetectors in `/pkg/engine/defaults/defaults.go`
6. Create a merge request for review. CI tests must be passing.
## Addendum
### Verification indeterminacy
There are two types of reasons that secret verification can fail:
* The candidate secret is not actually a valid secret.
* Something went wrong in the process unrelated to the candidate secret, such as a transient network error or an unexpected API response.
In TruffleHog parlance, the first type of verification response is called _determinate_ and the second type is called _indeterminate_. Verification code should distinguish between the two by returning an error object in the result struct **only** for indeterminate failures. In general, a verifier should return an error (indicating an indeterminate failure) in all cases that haven't been explicitly identified as determinate failure states.
For example, consider a hypothetical authentication endpoint that returns `200 OK` for valid credentials and `403 Forbidden` for invalid credentials. The verifier for this endpoint could make an HTTP request and use the response status code to decide what to return:
* A `200` response would indicate that verification succeeded. (Or maybe any `2xx` response.)
* A `403` response would indicate that verification failed **determinately** and no error object should be returned.
* Any other response would indicate that verification failed **indeterminately** and an error object should be returned.
### Managing Test Secrets
Do not embed test credentials in the test code. Instead, use GCP Secrets Manager.
1. Access the latest secret version for modification.
Note: `/tmp/s` is a valid path on Linux. You will need to change that for Windows or OSX, otherwise you will see an error. On Windows you will also need to install [WSL](https://docs.microsoft.com/en-us/windows/wsl/install).
```bash
gcloud secrets versions access --project trufflehog-testing --secret detectors5 latest > /tmp/s
```
2. Add the secret that you need for testing.
The command above saved it to `/tmp/s`.
The format is standard env file format,
```bash
SECRET_TYPE_ONE=value
SECRET_TYPE_ONE_INACTIVE=v@lue
```
3. Update the secret version with your modification.
```bash
gcloud secrets versions add --project trufflehog-testing detectors5 --data-file /tmp/s
```
Note: We increment the detectors file name `detectors(n+1)` once the previous one exceeds the max size allowed by GSM (65kb).
4. Access the secret value as shown in the [example code](pkg/detectors/heroku/heroku_test.go).
### Setting up Google Cloud SDK
1. Install the Google Cloud SDK: https://cloud.google.com/sdk/docs/install
2. Authenticate with `gcloud auth login --update-adc` using your Google account
### Adding Protos in Windows
1. Install Ubuntu App in Microsoft Store https://www.microsoft.com/en-us/p/ubuntu/9nblggh4msv6.
2. Install Docker Desktop https://www.docker.com/products/docker-desktop. Enable WSL integration to Ubuntu. In Docker app, go to Settings->Resources->WSL INTEGRATION->enable Ubuntu.
3. Open Ubuntu cli and install `dos2unix`.
```bash
sudo apt install dos2unix
```
4. Identify the `trufflehog` local directory and convert `scripts/gen_proto.sh` file in Unix format.
```bash
dos2unix ./scripts/gen_proto.sh
```
5. Open [/proto/detectors.proto](/proto/detectors.proto) file and add new detectors then save it. Make sure Docker is running and run this in Ubuntu command line.
```bash
make protos
```
### Testing a detector
```bash
go test ./pkg/detectors/ -tags=detectors
```
================================================
FILE: hack/docs/Adding_Detectors_external.md
================================================
# Secret Detectors
Secret Detectors have these two major functions:
1. Given some bytes, extract possible secrets, typically using a regex.
2. Validate the secrets against the target API, typically using a HTTP client.
The purpose of Secret Detectors is to discover secrets with exceptionally high signal. High rates of false positives are not accepted.
## Table of Contents
- [Secret Detectors](#secret-detectors)
* [Table of Contents](#table-of-contents)
* [Getting Started](#getting-started)
+ [Sourcing Guidelines](#sourcing-guidelines)
+ [Development Guidelines](#development-guidelines)
+ [Development Dependencies](#development-dependencies)
+ [Creating a new Secret Detector](#creating-a-new-secret-detector)
+ [Testing the Detector](#testing-the-detector)
* [Addendum](#addendum)
+ [Adding Protos in Windows](#adding-protos-in-windows)
## Getting Started
### Sourcing Guidelines
We are interested in detectors for services that meet at least one of these criteria
- host data (they store any sort of data provided)
- have paid services (having a free or trial tier is okay though)
If you think that something should be included outside of these guidelines, please let us know.
### Development Guidelines
- When reasonable, favor using the `net/http` library to make requests instead of bringing in another library.
- Use the [`common.SaneHttpClient`](/pkg/common/http.go) for the `http.Client` whenever possible.
### Development Dependencies
- Go 1.17+
- Make
### Adding New Token Formats to an Existing Scanner
In some instances, services will update their token format, requiring a new regex to properly detect secrets in addition to supporting the previous token format. Accommodating this can be done without adding a net-new detector. [We provide a `Versioner` interface](https://github.com/trufflesecurity/trufflehog/blob/e18cfd5e0af1469a9f05b8d5732bcc94c39da49c/pkg/detectors/detectors.go#L30) that can be implemented.
1. Create two new directories `v1` and `v2`. Move the existing detector and tests into `v1`, and add new files to `v2`.
Ex: `/` -> `/v1/`, `/v2/`
Note: Be sure to update the tests to reference the new secret values in GSM, or the tests will fail.
2. Implement the `Versioner` interface. [GitHub example implementation.](https://github.com/trufflesecurity/trufflehog/blob/2964b3b2d2edf2b60b1f71443338c6534720b67a/pkg/detectors/github/v1/github_old.go#L23))
3. Add a 'version' field in ExtraData for both existing and new detector versions.
4. Update the existing detector in DefaultDetectors in `/pkg/engine/defaults/defaults.go`
5. Proceed from step 3 of [Creating a new Secret Scanner](#creating-a-new-secret-scanner)
### Creating a new Secret Detector
1. Add a new Secret Detector enum to the [`DetectorType` list here](/proto/detectors.proto).
2. Run `make protos` to update the `.pb` files.
3. Generate the Secret Detector
```bash
go run hack/generate/generate.go detector
example: go run hack/generate/generate.go detector SampleAPI
```
4. Add the Secret Detector to TruffleHog's Default Detectors
Add the secret scanner to the [`pkg/engine/defaults/defaults.go`](https://github.com/trufflesecurity/trufflehog/blob/main/pkg/engine/defaults/defaults.go) file like [`github.com/trufflesecurity/trufflehog/v3/pkg/detectors/`](https://github.com/trufflesecurity/trufflehog/blob/b71ea27a696bdf1c3141f637fda4ee4936c2f2d6/pkg/engine/defaults/defaults.go#L9) and
[`.Scanner{}`](https://github.com/trufflesecurity/trufflehog/blob/b71ea27a696bdf1c3141f637fda4ee4936c2f2d6/pkg/engine/defaults/defaults.go#L1546)
5. Complete the Secret Detector.
The previous step templated a boilerplate + some example code as a package in the `pkg/detectors` folder for you to work on.
The Secret Detector can be completed with these general steps:
1. Update the pattern regex and keywords. Try iterating with [regex101.com](http://regex101.com/).
2. Update the verifier code to use a non-destructive API call that can determine whether the secret is valid or not.
* Make sure you understand [verification indeterminacy](#verification-indeterminacy).
3. Create a [test for the detector](#testing-the-detector).
4. Add your new detector to DefaultDetectors in `/pkg/engine/defaults/defaults.go`.
5. Create a pull request for review.
### Testing the Detector
To ensure the quality of your PR, make sure your tests are passing with verified credentials.
1. Create a file called `.env` with this env file format:
```bash
SECRET_TYPE_ONE=value
SECRET_TYPE_ONE_INACTIVE=v@lue
```
2. Export the `TEST_SECRET_FILE` variable, pointing to the env file:
```bash
export TEST_SECRET_FILE=".env"
```
The `.env` file should be in the new detector's directory like this:
```
├── tailscale
│ ├── .env
│ ├── tailscale.go
│ └── tailscale_test.go
```
Now that a `.env` file is present, the test file can load secrets locally.
3. Next, update the tests as necessary. A test file has already been generated by the `go run hack/generate/generate.go` command from earlier. There are 5 cases that have been generated:
1. Found and verified (using a credential loaded from the .env file)
2. Found and unverified (determinately, i.e. the secret is invalid)
3. Found and unverified (indeterminately due to timeout)
4. Found and unverified (indeterminately due to an unexpected API response)
5. Not found
Make any necessary updates to the tests. Note there might not be any changes required as the tests generated by the `go run hack/generate/generate.go` command are pretty good.
[Here is an exemplary test file for a detector which covers all 5 test cases](https://github.com/trufflesecurity/trufflehog/blob/6f9065b0aae981133a7fa3431c17a5c6213be226/pkg/detectors/browserstack/browserstack_test.go).
4. Now run the tests and check to make sure they are passing ✔️!
```bash
go test ./pkg/detectors/ -tags=detectors
```
If the tests are passing, feel free to open a PR!
## Addendum
### Verification indeterminacy
There are two types of reasons that secret verification can fail:
* The candidate secret is not actually a valid secret.
* Something went wrong in the process unrelated to the candidate secret, such as a transient network error or an unexpected API response.
In TruffleHog parlance, the first type of verification response is called _determinate_ and the second type is called _indeterminate_. Verification code should distinguish between the two by returning an error object in the result struct **only** for indeterminate failures. In general, a verifier should return an error (indicating an indeterminate failure) in all cases that haven't been explicitly identified as determinate failure states.
For example, consider a hypothetical authentication endpoint that returns `200 OK` for valid credentials and `403 Forbidden` for invalid credentials. The verifier for this endpoint could make an HTTP request and use the response status code to decide what to return:
* A `200` response would indicate that verification succeeded. (Or maybe any `2xx` response.)
* A `403` response would indicate that verification failed **determinately** and no error object should be returned.
* Any other response would indicate that verification failed **indeterminately** and an error object should be returned.
### Adding Protos in Windows
1. Install Ubuntu App in Microsoft Store https://www.microsoft.com/en-us/p/ubuntu/9nblggh4msv6.
2. Install Docker Desktop https://www.docker.com/products/docker-desktop. Enable WSL integration to Ubuntu. In Docker app, go to Settings->Resources->WSL INTEGRATION->enable Ubuntu.
3. Open Ubuntu cli and install `dos2unix`.
```bash
sudo apt install dos2unix
```
4. Identify the `trufflehog` local directory and convert `scripts/gen_proto.sh` file in Unix format.
```bash
dos2unix ./scripts/gen_proto.sh
```
5. Open [/proto/detectors.proto](/proto/detectors.proto) file and add new detectors then save it. Make sure Docker is running and run this in Ubuntu command line.
```bash
make protos
```
================================================
FILE: hack/generate/generate.go
================================================
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/alecthomas/kingpin/v2"
"github.com/go-errors/errors"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var (
app = kingpin.New("generate", "Generate is used to write new features.")
kind = app.Arg("kind", "Kind of thing to generate.").Required().Enum("detector")
name = app.Arg("name", "Name of the Source/Detector to generate.").Required().String()
nameTitle, nameLower, nameUpper string
)
func main() {
log.SetFlags(log.Lmsgprefix)
log.SetPrefix("😲 [generate] ")
kingpin.MustParse(app.Parse(os.Args[1:]))
nameTitle = cases.Title(language.AmericanEnglish).String(*name)
nameLower = strings.ToLower(*name)
nameUpper = strings.ToUpper(*name)
switch *kind {
case "detector":
mustWriteTemplates([]templateJob{
{
TemplatePath: "pkg/detectors/alchemy/alchemy.go",
WritePath: filepath.Join(folderPath(), nameLower+".go"),
ReplaceString: []string{"alchemy"},
},
{
TemplatePath: "pkg/detectors/alchemy/alchemy_test.go",
WritePath: filepath.Join(folderPath(), nameLower+"_test.go"),
ReplaceString: []string{"alchemy"},
},
{
TemplatePath: "pkg/detectors/alchemy/alchemy_integration_test.go",
WritePath: filepath.Join(folderPath(), nameLower+"_integration_test.go"),
ReplaceString: []string{"alchemy"},
},
})
// case "source":
// mustWriteTemplates([]templateJob{
// {
// TemplatePath: "pkg/sources/filesystem/filesystem.go",
// WritePath: filepath.Join(folderPath(), nameLower+".go"),
// ReplaceString: []string{"filesystem"},
// },
// {
// TemplatePath: "pkg/sources/filesystem/filesystem_test.go",
// WritePath: filepath.Join(folderPath(), nameLower+"_test.go"),
// ReplaceString: []string{"filesystem"},
// },
// })
}
}
type templateJob struct {
TemplatePath string
WritePath string
ReplaceString []string
}
func mustWriteTemplates(jobs []templateJob) {
log.Printf("Generating %s %s\n", cases.Title(language.AmericanEnglish).String(*kind), nameTitle)
// Make the folder.
log.Printf("Creating folder %s\n", folderPath())
err := makeFolder(folderPath())
if err != nil {
log.Fatal(err)
}
// Write the files from templates.
for _, job := range jobs {
tmplBytes, err := os.ReadFile(job.TemplatePath)
if err != nil {
log.Fatal(err)
}
tmplRaw := string(tmplBytes)
for _, rplString := range job.ReplaceString {
rplTitle := cases.Title(language.AmericanEnglish).String(rplString)
tmplRaw = strings.ReplaceAll(tmplRaw, "DetectorType_"+rplTitle, "DetectorType_<<.Name>>")
tmplRaw = strings.ReplaceAll(tmplRaw, strings.ToLower(rplString), "<<.NameLower>>")
tmplRaw = strings.ReplaceAll(tmplRaw, rplTitle, "<<.NameTitle>>")
tmplRaw = strings.ReplaceAll(tmplRaw, strings.ToUpper(rplString), "<<.NameUpper>>")
}
tmpl := template.Must(template.New("main").Delims("<<", ">>").Parse(tmplRaw))
log.Printf("Writing file %s\n", job.WritePath)
f, err := os.OpenFile(job.WritePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
log.Fatal(err)
}
err = tmpl.Execute(f, templateData{
Name: *name,
NameTitle: nameTitle,
NameLower: nameLower,
NameUpper: nameUpper,
})
if err != nil {
log.Fatal(fmt.Errorf("failed to execute template: %w", err))
}
}
}
type templateData struct {
Name string
NameTitle string
NameLower string
NameUpper string
}
func folderPath() string {
return filepath.Join("pkg/", *kind+"s", nameLower)
}
func makeFolder(path string) error {
_, err := os.Stat(path)
if os.IsNotExist(err) {
err := os.MkdirAll(path, 0755)
if err != nil {
return errors.New(err)
}
return nil
}
return errors.Errorf("%s %s already exists", *kind, *name)
}
================================================
FILE: hack/generate/test.sh
================================================
#!/usr/bin/env bash
set -eu
function cleanup {
rm -rf pkg/detectors/test
}
trap cleanup EXIT
export CGO_ENABLED=0
export FORCE_PASS_DIFF=true
echo "████████████ Testing generate Detector"
go run hack/generate/generate.go detector Test
go test ./pkg/detectors/test -benchmem -bench .
echo ""
================================================
FILE: hack/semgrep-rules/detectors.yaml
================================================
rules:
- id: no-printing-in-detectors
patterns:
- pattern-either:
- pattern: fmt.Println(...)
- pattern: fmt.Printf(...)
- pattern: import("log")
message: "Do not print or log inside of detectors."
languages: [go]
severity: ERROR
================================================
FILE: hack/snifftest/README.md
================================================
# snifftest
See the help pages with this command, or look further below to get started quickly.
```
go run hack/snifftest/main.go
```
## Show available secret scanners
```
go run hack/snifftest/main.go show-scanners
```
## Scan
All scanners
```
go run snifftest/main.go scan --db ~/sdb --scanner all --print
```
Particular scanner
```
go run snifftest/main.go scan --db ~/sdb --scanner github --print --print-chunk --fail-threshold 5
```
================================================
FILE: hack/snifftest/main.go
================================================
package main
import (
"fmt"
"os"
"reflect"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/alecthomas/kingpin/v2"
"github.com/paulbellamy/ratecounter"
"golang.org/x/sync/semaphore"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/decoders"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/defaults"
"github.com/trufflesecurity/trufflehog/v3/pkg/log"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/git"
)
var (
// CLI flags and commands
app = kingpin.New("Snifftest", "Test secret detectors against data sets.")
showDetectorsCmd = app.Command("show-detectors", "Shows the available detectors.")
scanCmd = app.Command("scan", "Scans data.")
scanCmdDetector = scanCmd.Flag("detector", "Detector to scan with. 'all', or a specific name.").Default("all").String()
scanCmdExclude = scanCmd.Flag("exclude", "Detector(s) to exclude").Strings()
scanCmdRepo = scanCmd.Flag("repo", "URI to .git repo.").Required().String()
scanThreshold = scanCmd.Flag("fail-threshold", "Result threshold that causes failure for a single scanner.").Int()
scanPrintRes = scanCmd.Flag("print", "Print results.").Bool()
scanPrintChunkRes = scanCmd.Flag("print-chunk", "Print chunks that have results.").Bool()
scanVerify = scanCmd.Flag("verify", "Verify found secrets.").Bool()
)
func main() {
// setup logger
logger, flush := log.New("trufflehog", log.WithConsoleSink(os.Stderr))
// make it the default logger for contexts
context.SetDefaultLogger(logger)
defer func() { _ = flush() }()
logFatal := func(err error, message string, keyAndVals ...any) {
logger.Error(err, message, keyAndVals...)
if err != nil {
os.Exit(1)
return
}
os.Exit(0)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Hour*2)
var cancelOnce sync.Once
defer cancelOnce.Do(cancel)
cmd := kingpin.MustParse(app.Parse(os.Args[1:]))
switch cmd {
case scanCmd.FullCommand():
chunksChan := make(chan *sources.Chunk, 10000)
var wgChunkers sync.WaitGroup
sem := semaphore.NewWeighted(int64(runtime.NumCPU()))
selectedScanners := map[string]detectors.Detector{}
allScanners := getAllScanners()
allDecoders := decoders.DefaultDecoders()
input := strings.ToLower(*scanCmdDetector)
if input == "all" {
selectedScanners = allScanners
} else {
_, ok := allScanners[input]
if !ok {
logFatal(fmt.Errorf("invalid input"), "could not find scanner by that name")
}
selectedScanners[input] = allScanners[input]
}
if len(selectedScanners) == 0 {
logFatal(fmt.Errorf("invalid input"), "no detectors selected")
}
for _, excluded := range *scanCmdExclude {
delete(selectedScanners, excluded)
}
logger.Info("loaded secret detectors", "count", len(selectedScanners)+3)
var wgScanners sync.WaitGroup
var chunkCounter uint64
go func() {
counter := ratecounter.NewRateCounter(60 * time.Second)
var prev uint64
for {
time.Sleep(60 * time.Second)
counter.Incr(int64(chunkCounter - prev))
prev = chunkCounter
logger.Info("chunk scan rate per second", "rate", counter.Rate()/60)
}
}()
resCounter := make(map[string]*uint64)
failed := false
for i := 0; i < runtime.NumCPU(); i++ {
wgScanners.Add(1)
go func() {
defer wgScanners.Done()
for chunk := range chunksChan {
for name, scanner := range selectedScanners {
for _, dec := range allDecoders {
decoded := dec.FromChunk(&sources.Chunk{Data: chunk.Data})
if decoded != nil {
foundKeyword := false
for _, kw := range scanner.Keywords() {
if strings.Contains(strings.ToLower(string(decoded.Data)), strings.ToLower(kw)) {
foundKeyword = true
}
}
if !foundKeyword {
continue
}
res, err := scanner.FromData(ctx, *scanVerify, decoded.Data)
if err != nil {
logFatal(err, "error scanning chunk")
}
if len(res) > 0 {
if resCounter[name] == nil {
zero := uint64(0)
resCounter[name] = &zero
}
atomic.AddUint64(resCounter[name], uint64(len(res)))
if *scanThreshold != 0 && int(*resCounter[name]) > *scanThreshold {
logger.Error(
fmt.Errorf("exceeded result threshold"), "snifftest failed",
"scanner", name, "threshold", *scanThreshold,
)
failed = true
os.Exit(1)
}
if *scanPrintRes {
for _, r := range res {
logger := logger.WithValues("secret", name, "meta", chunk.SourceMetadata, "result", string(r.Raw))
if *scanPrintChunkRes {
logger = logger.WithValues("chunk", string(decoded.Data))
}
logger.Info("result")
}
}
}
}
}
}
atomic.AddUint64(&chunkCounter, uint64(1))
}
}()
}
for _, repo := range strings.Split(*scanCmdRepo, ",") {
if err := sem.Acquire(ctx, 1); err != nil {
logFatal(err, "timed out waiting for semaphore")
}
wgChunkers.Add(1)
go func(r string) {
defer sem.Release(1)
defer wgChunkers.Done()
logger.Info("cloning repo", "repo", r)
path, repo, err := git.CloneRepoUsingUnauthenticated(ctx, r, "")
if err != nil {
logFatal(err, "error cloning repo", "repo", r)
}
logger.Info("cloned repo", "repo", r)
cfg := &git.Config{
SourceName: "snifftest",
JobID: 0,
SourceID: 0,
SourceType: sourcespb.SourceType_SOURCE_TYPE_GIT,
Verify: false,
SkipBinaries: true,
SkipArchives: false,
Concurrency: runtime.NumCPU(),
SourceMetadataFunc: func(file, email, commit, timestamp, repository, repositoryLocalPath string, line int64) *source_metadatapb.MetaData {
return &source_metadatapb.MetaData{
Data: &source_metadatapb.MetaData_Git{
Git: &source_metadatapb.Git{
Commit: commit,
File: file,
Email: email,
Repository: repository,
Timestamp: timestamp,
},
},
}
},
}
s := git.NewGit(cfg)
logger.Info("scanning repo", "repo", r)
err = s.ScanRepo(ctx, repo, path, git.NewScanOptions(), sources.ChanReporter{Ch: chunksChan})
if err != nil {
logFatal(err, "error scanning repo")
}
logger.Info("scanned repo", "repo", r)
defer os.RemoveAll(path)
}(repo)
}
go func() {
wgChunkers.Wait()
close(chunksChan)
}()
wgScanners.Wait()
logger.Info("completed snifftest", "chunks", chunkCounter)
for scanner, resultsCount := range resCounter {
logger.Info(scanner, "results", *resultsCount)
}
if failed {
os.Exit(1)
}
case showDetectorsCmd.FullCommand():
for s := range getAllScanners() {
fmt.Println(s)
}
}
}
func getAllScanners() map[string]detectors.Detector {
allScanners := map[string]detectors.Detector{}
for _, s := range defaults.DefaultDetectors() {
secretType := reflect.Indirect(reflect.ValueOf(s)).Type().PkgPath()
path := strings.Split(secretType, "/")[len(strings.Split(secretType, "/"))-1]
allScanners[path] = s
}
return allScanners
}
================================================
FILE: hack/snifftest/snifftest.sh
================================================
#!/usr/bin/env bash
REPO_ARRAY=(
"https://github.com/Netflix/Hystrix.git"
# "https://github.com/facebook/flow.git"
# "https://github.com/Netflix/vizceral.git"
# "https://github.com/Netflix/metaflow.git"
# "https://github.com/Netflix/dgs-framework.git"
# "https://github.com/Netflix/vector.git"
# "https://github.com/expressjs/express.git"
# "https://github.com/Azure/azure-sdk-for-net"
# "https://github.com/Azure/azure-cli"
)
REPOS=$(printf "%s," "${REPO_ARRAY[@]}" | cut -d "," -f 1-${#REPO_ARRAY[@]})
go run hack/snifftest/main.go scan --exclude privatekey --exclude uri --exclude github_old --repo "$REPOS" --detector all --print --fail-threshold 99
================================================
FILE: main.go
================================================
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
_ "net/http/pprof"
"os"
"os/exec"
"os/signal"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"github.com/alecthomas/kingpin/v2"
"github.com/fatih/color"
"github.com/felixge/fgprof"
"github.com/go-logr/logr"
"github.com/jpillora/overseer"
"github.com/mattn/go-isatty"
"go.uber.org/automaxprocs/maxprocs"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer"
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
"github.com/trufflesecurity/trufflehog/v3/pkg/cleantemp"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/defaults"
"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
"github.com/trufflesecurity/trufflehog/v3/pkg/handlers"
"github.com/trufflesecurity/trufflehog/v3/pkg/log"
"github.com/trufflesecurity/trufflehog/v3/pkg/output"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
"github.com/trufflesecurity/trufflehog/v3/pkg/tui"
"github.com/trufflesecurity/trufflehog/v3/pkg/updater"
"github.com/trufflesecurity/trufflehog/v3/pkg/verificationcache"
"github.com/trufflesecurity/trufflehog/v3/pkg/version"
)
var (
cli = kingpin.New("TruffleHog", "TruffleHog is a tool for finding credentials.")
cmd string
// https://github.com/trufflesecurity/trufflehog/blob/main/CONTRIBUTING.md#logging-in-trufflehog
logLevel = cli.Flag("log-level", `Logging verbosity on a scale of 0 (info) to 5 (trace). Can be disabled with "-1".`).Default("0").Int()
debug = cli.Flag("debug", "Run in debug mode.").Hidden().Bool()
trace = cli.Flag("trace", "Run in trace mode.").Hidden().Bool()
profile = cli.Flag("profile", "Enables profiling and sets a pprof and fgprof server on :18066.").Bool()
localDev = cli.Flag("local-dev", "Hidden feature to disable overseer for local dev.").Hidden().Bool()
jsonOut = cli.Flag("json", "Output in JSON format.").Short('j').Bool()
jsonLegacy = cli.Flag("json-legacy", "Use the pre-v3.0 JSON format. Only works with git, gitlab, and github sources.").Bool()
gitHubActionsFormat = cli.Flag("github-actions", "Output in GitHub Actions format.").Bool()
concurrency = cli.Flag("concurrency", "Number of concurrent workers.").Default(strconv.Itoa(runtime.NumCPU())).Int()
noVerification = cli.Flag("no-verification", "Don't verify the results.").Bool()
onlyVerified = cli.Flag("only-verified", "Only output verified results.").Hidden().Bool()
results = cli.Flag("results", "Specifies which type(s) of results to output: verified (confirmed valid by API), unknown (verification failed due to error), unverified (detected but not verified), filtered_unverified (unverified but would have been filtered out). Defaults to verified,unverified,unknown.").String()
noColor = cli.Flag("no-color", "Disable colorized output").Bool()
noColour = cli.Flag("no-colour", "Alias for --no-color").Hidden().Bool()
allowVerificationOverlap = cli.Flag("allow-verification-overlap", "Allow verification of similar credentials across detectors").Bool()
filterUnverified = cli.Flag("filter-unverified", "Only output first unverified result per chunk per detector if there are more than one results.").Bool()
filterEntropy = cli.Flag("filter-entropy", "Filter unverified results with Shannon entropy. Start with 3.0.").Float64()
scanEntireChunk = cli.Flag("scan-entire-chunk", "Scan the entire chunk for secrets.").Hidden().Default("false").Bool()
maxDecodeDepth = cli.Flag("max-decode-depth", "Maximum depth of iterative decoding. Each decoder's output is fed back through all decoders, up to this limit. 1 = single pass, 2+ = chained decoding (e.g., base64 inside utf16).").Default("5").Int()
compareDetectionStrategies = cli.Flag("compare-detection-strategies", "Compare different detection strategies for matching spans").Hidden().Default("false").Bool()
configFilename = cli.Flag("config", "Path to configuration file.").ExistingFile()
// rules = cli.Flag("rules", "Path to file with custom rules.").String()
printAvgDetectorTime = cli.Flag("print-avg-detector-time", "Print the average time spent on each detector.").Bool()
noUpdate = cli.Flag("no-update", "Don't check for updates.").Bool()
fail = cli.Flag("fail", "Exit with code 183 if results are found.").Bool()
failOnScanErrors = cli.Flag("fail-on-scan-errors", "Exit with non-zero error code if an error occurs during the scan.").Bool()
verifiers = cli.Flag("verifier", "Set custom verification endpoints.").StringMap()
customVerifiersOnly = cli.Flag("custom-verifiers-only", "Only use custom verification endpoints.").Bool()
detectorTimeout = cli.Flag("detector-timeout", "Maximum time to spend scanning chunks per detector (e.g., 30s).").Duration()
archiveMaxSize = cli.Flag("archive-max-size", "Maximum size of archive to scan. (Byte units eg. 512B, 2KB, 4MB)").Bytes()
archiveMaxDepth = cli.Flag("archive-max-depth", "Maximum depth of archive to scan.").Int()
archiveTimeout = cli.Flag("archive-timeout", "Maximum time to spend extracting an archive.").Duration()
includeDetectors = cli.Flag("include-detectors", "Comma separated list of detector types to include. Protobuf name or IDs may be used, as well as ranges.").Default("all").String()
excludeDetectors = cli.Flag("exclude-detectors", "Comma separated list of detector types to exclude. Protobuf name or IDs may be used, as well as ranges. IDs defined here take precedence over the include list.").String()
jobReportFile = cli.Flag("output-report", "Write a scan report to the provided path.").Hidden().OpenFile(os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
noVerificationCache = cli.Flag("no-verification-cache", "Disable verification caching").Bool()
// Add feature flags
forceSkipBinaries = cli.Flag("force-skip-binaries", "Force skipping binaries.").Bool()
forceSkipArchives = cli.Flag("force-skip-archives", "Force skipping archives.").Bool()
gitCloneTimeout = cli.Flag("git-clone-timeout", "Maximum time to spend cloning a repository, as a duration.").Hidden().Duration()
skipAdditionalRefs = cli.Flag("skip-additional-refs", "Skip additional references.").Bool()
userAgentSuffix = cli.Flag("user-agent-suffix", "Suffix to add to User-Agent.").String()
gitScan = cli.Command("git", "Find credentials in git repositories.")
gitScanURI = gitScan.Arg("uri", "Git repository URL. https://, file://, or ssh:// schema expected.").Required().String()
gitScanIncludePaths = gitScan.Flag("include-paths", "Path to file with newline separated regexes for files to include in scan.").Short('i').String()
gitScanExcludePaths = gitScan.Flag("exclude-paths", "Path to file with newline separated regexes for files to exclude in scan.").Short('x').String()
gitScanExcludeGlobs = gitScan.Flag("exclude-globs", "Comma separated list of globs to exclude in scan. This option filters at the `git log` level, resulting in faster scans.").String()
gitScanSinceCommit = gitScan.Flag("since-commit", "Commit to start scan from.").String()
gitScanBranch = gitScan.Flag("branch", "Branch to scan.").String()
gitScanMaxDepth = gitScan.Flag("max-depth", "Maximum depth of commits to scan.").Int()
gitScanBare = gitScan.Flag("bare", "Scan bare repository (e.g. useful while using in pre-receive hooks)").Bool()
gitClonePath = gitScan.Flag("clone-path", "Custom path where the repository should be cloned (default: temp dir).").String()
gitNoCleanup = gitScan.Flag("no-cleanup", "Do not delete cloned repositories after scanning (can only be used with --clone-path).").Bool()
gitTrustLocalGitConfig = gitScan.Flag("trust-local-git-config", "Trust local git config.").Bool()
_ = gitScan.Flag("allow", "No-op flag for backwards compat.").Bool()
_ = gitScan.Flag("entropy", "No-op flag for backwards compat.").Bool()
_ = gitScan.Flag("regex", "No-op flag for backwards compat.").Bool()
githubScan = cli.Command("github", "Find credentials in GitHub repositories.")
githubScanEndpoint = githubScan.Flag("endpoint", "GitHub endpoint.").Default("https://api.github.com").String()
githubScanRepos = githubScan.Flag("repo", `GitHub repository to scan. You can repeat this flag. Example: "https://github.com/dustin-decker/secretsandstuff"`).Strings()
githubScanOrgs = githubScan.Flag("org", `GitHub organization to scan. You can repeat this flag. Example: "trufflesecurity"`).Strings()
githubScanToken = githubScan.Flag("token", "GitHub token. Can be provided with environment variable GITHUB_TOKEN.").Envar("GITHUB_TOKEN").String()
githubIncludeForks = githubScan.Flag("include-forks", "Include forks in scan.").Bool()
githubIncludeMembers = githubScan.Flag("include-members", "Include organization member repositories in scan.").Bool()
githubIncludeRepos = githubScan.Flag("include-repos", `Repositories to include in an org scan. This can also be a glob pattern. You can repeat this flag. Must use Github repo full name. Example: "trufflesecurity/trufflehog", "trufflesecurity/t*"`).Strings()
githubIncludeWikis = githubScan.Flag("include-wikis", "Include repository wikisin scan.").Bool()
githubExcludeRepos = githubScan.Flag("exclude-repos", `Repositories to exclude in an org scan. This can also be a glob pattern. You can repeat this flag. Must use Github repo full name. Example: "trufflesecurity/driftwood", "trufflesecurity/d*"`).Strings()
githubScanIncludePaths = githubScan.Flag("include-paths", "Path to file with newline separated regexes for files to include in scan.").Short('i').String()
githubScanExcludePaths = githubScan.Flag("exclude-paths", "Path to file with newline separated regexes for files to exclude in scan.").Short('x').String()
githubScanIssueComments = githubScan.Flag("issue-comments", "Include issue descriptions and comments in scan.").Bool()
githubScanPRComments = githubScan.Flag("pr-comments", "Include pull request descriptions and comments in scan.").Bool()
githubScanGistComments = githubScan.Flag("gist-comments", "Include gist comments in scan.").Bool()
githubCommentsTimeframeDays = githubScan.Flag("comments-timeframe", "Number of days in the past to review when scanning issue, PR, and gist comments.").Uint32()
githubAuthInUrl = githubScan.Flag("auth-in-url", "Embed authentication credentials in repository URLs instead of using secure HTTP headers").Bool()
githubClonePath = githubScan.Flag("clone-path", "Custom path where the repository should be cloned (default: temp dir).").String()
githubNoCleanup = githubScan.Flag("no-cleanup", "Do not delete cloned repositories after scanning (can only be used with --clone-path).").Bool()
githubIgnoreGists = githubScan.Flag("ignore-gists", "Ignore all gists in scan.").Bool()
// GitHub Cross Fork Object Reference Experimental Feature
githubExperimentalScan = cli.Command("github-experimental", "Run an experimental GitHub scan. Must specify at least one experimental sub-module to run: object-discovery.")
// GitHub Experimental SubModules
githubExperimentalObjectDiscovery = githubExperimentalScan.Flag("object-discovery", "Discover hidden data objects in GitHub repositories.").Bool()
// GitHub Experimental Options
githubExperimentalToken = githubExperimentalScan.Flag("token", "GitHub token. Can be provided with environment variable GITHUB_TOKEN.").Envar("GITHUB_TOKEN").String()
githubExperimentalRepo = githubExperimentalScan.Flag("repo", "GitHub repository to scan. Example: https://github.com//.git").Required().String()
githubExperimentalCollisionThreshold = githubExperimentalScan.Flag("collision-threshold", "Threshold for short-sha collisions in object-discovery submodule. Default is 1.").Default("1").Int()
githubExperimentalDeleteCache = githubExperimentalScan.Flag("delete-cached-data", "Delete cached data after object-discovery secret scanning.").Bool()
gitlabScan = cli.Command("gitlab", "Find credentials in GitLab repositories.")
// TODO: Add more GitLab options
gitlabScanEndpoint = gitlabScan.Flag("endpoint", "GitLab endpoint.").Default("https://gitlab.com").String()
gitlabScanRepos = gitlabScan.Flag("repo", "GitLab repo url. You can repeat this flag. Leave empty to scan all repos accessible with provided credential. Example: https://gitlab.com/org/repo.git").Strings()
gitlabScanToken = gitlabScan.Flag("token", "GitLab token. Can be provided with environment variable GITLAB_TOKEN.").Envar("GITLAB_TOKEN").Required().String()
gitlabScanGroupIds = gitlabScan.Flag("group-id", "GitLab group ID. If provided, it will scan the group and its subgroups. You can repeat this flag.").Strings()
gitlabScanIncludePaths = gitlabScan.Flag("include-paths", "Path to file with newline separated regexes for files to include in scan.").Short('i').String()
gitlabScanExcludePaths = gitlabScan.Flag("exclude-paths", "Path to file with newline separated regexes for files to exclude in scan.").Short('x').String()
gitlabScanIncludeRepos = gitlabScan.Flag("include-repos", `Repositories to include in an org scan. This can also be a glob pattern. You can repeat this flag. Must use Gitlab repo full name. Example: "trufflesecurity/trufflehog", "trufflesecurity/t*"`).Strings()
gitlabScanExcludeRepos = gitlabScan.Flag("exclude-repos", `Repositories to exclude in an org scan. This can also be a glob pattern. You can repeat this flag. Must use Gitlab repo full name. Example: "trufflesecurity/driftwood", "trufflesecurity/d*"`).Strings()
gitlabAuthInUrl = gitlabScan.Flag("auth-in-url", "Embed authentication credentials in repository URLs instead of using secure HTTP headers").Bool()
gitlabClonePath = gitlabScan.Flag("clone-path", "Custom path where the repository should be cloned (default: temp dir)").String()
gitlabNoCleanup = gitlabScan.Flag("no-cleanup", "Do not delete cloned repositories after scanning (can only be used with --clone-path).").Bool()
filesystemScan = cli.Command("filesystem", "Find credentials in a filesystem.")
filesystemPaths = filesystemScan.Arg("path", "Path to file or directory to scan.").Strings()
// DEPRECATED: --directory is deprecated in favor of arguments.
filesystemDirectories = filesystemScan.Flag("directory", "Path to directory to scan. You can repeat this flag.").Strings()
// TODO: Add more filesystem scan options. Currently only supports scanning a list of directories.
// filesystemScanRecursive = filesystemScan.Flag("recursive", "Scan recursively.").Short('r').Bool()
filesystemScanIncludePaths = filesystemScan.Flag("include-paths", "Path to file with newline separated regexes for files to include in scan.").Short('i').String()
filesystemScanExcludePaths = filesystemScan.Flag("exclude-paths", "Path to file with newline separated regexes for files to exclude in scan.").Short('x').String()
filesystemScanMaxSymlinkDepth = filesystemScan.Flag("max-symlink-depth", "Maximum depth to follow symlinks during filesystem scan.").Short('s').Int32()
s3Scan = cli.Command("s3", "Find credentials in S3 buckets.")
s3ScanKey = s3Scan.Flag("key", "S3 key used to authenticate. Can be provided with environment variable AWS_ACCESS_KEY_ID.").Envar("AWS_ACCESS_KEY_ID").String()
s3ScanRoleArns = s3Scan.Flag("role-arn", "Specify the ARN of an IAM role to assume for scanning. You can repeat this flag.").Strings()
s3ScanSecret = s3Scan.Flag("secret", "S3 secret used to authenticate. Can be provided with environment variable AWS_SECRET_ACCESS_KEY.").Envar("AWS_SECRET_ACCESS_KEY").String()
s3ScanSessionToken = s3Scan.Flag("session-token", "S3 session token used to authenticate temporary credentials. Can be provided with environment variable AWS_SESSION_TOKEN.").Envar("AWS_SESSION_TOKEN").String()
s3ScanCloudEnv = s3Scan.Flag("cloud-environment", "Use IAM credentials in cloud environment.").Bool()
s3ScanBuckets = s3Scan.Flag("bucket", "Name of S3 bucket to scan. You can repeat this flag. Incompatible with --ignore-bucket.").Strings()
s3ScanIgnoreBuckets = s3Scan.Flag("ignore-bucket", "Name of S3 bucket to ignore. You can repeat this flag. Incompatible with --bucket.").Strings()
s3ScanMaxObjectSize = s3Scan.Flag("max-object-size", "Maximum size of objects to scan. Objects larger than this will be skipped. (Byte units eg. 512B, 2KB, 4MB)").Default("250MB").Bytes()
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()
gcsServiceAccount = gcsScan.Flag("service-account", "Path to GCS service account JSON file.").ExistingFile()
gcsWithoutAuth = gcsScan.Flag("without-auth", "Scan GCS buckets without authentication. This will only work for public buckets").Bool()
gcsAPIKey = gcsScan.Flag("api-key", "GCS API key used to authenticate. Can be provided with environment variable GOOGLE_API_KEY.").Envar("GOOGLE_API_KEY").String()
gcsIncludeBuckets = gcsScan.Flag("include-buckets", "Buckets to scan. Comma separated list of buckets. You can repeat this flag. Globs are supported").Short('I').Strings()
gcsExcludeBuckets = gcsScan.Flag("exclude-buckets", "Buckets to exclude from scan. Comma separated list of buckets. Globs are supported").Short('X').Strings()
gcsIncludeObjects = gcsScan.Flag("include-objects", "Objects to scan. Comma separated list of objects. you can repeat this flag. Globs are supported").Short('i').Strings()
gcsExcludeObjects = gcsScan.Flag("exclude-objects", "Objects to exclude from scan. Comma separated list of objects. You can repeat this flag. Globs are supported").Short('x').Strings()
gcsMaxObjectSize = gcsScan.Flag("max-object-size", "Maximum size of objects to scan. Objects larger than this will be skipped. (Byte units eg. 512B, 2KB, 4MB)").Default("10MB").Bytes()
syslogScan = cli.Command("syslog", "Scan syslog")
syslogAddress = syslogScan.Flag("address", "Address and port to listen on for syslog. Example: 127.0.0.1:514").String()
syslogProtocol = syslogScan.Flag("protocol", "Protocol to listen on. udp or tcp").String()
syslogTLSCert = syslogScan.Flag("cert", "Path to TLS cert.").String()
syslogTLSKey = syslogScan.Flag("key", "Path to TLS key.").String()
syslogFormat = syslogScan.Flag("format", "Log format. Can be rfc3164 or rfc5424").Required().String()
circleCiScan = cli.Command("circleci", "Scan CircleCI")
circleCiScanToken = circleCiScan.Flag("token", "CircleCI token. Can also be provided with environment variable").Envar("CIRCLECI_TOKEN").Required().String()
dockerScan = cli.Command("docker", "Scan Docker Image")
dockerScanImages = dockerScan.Flag("image", "Docker image to scan. Use the file:// prefix to point to a local tarball, the docker:// prefix to point to the docker daemon, otherwise an image registry is assumed.").Strings()
dockerScanToken = dockerScan.Flag("token", "Docker bearer token. Can also be provided with environment variable").Envar("DOCKER_TOKEN").String()
dockerExcludePaths = dockerScan.Flag("exclude-paths", "Comma separated list of paths to exclude from scan").String()
dockerScanNamespace = dockerScan.Flag("namespace", "Docker namespace (organization or user). For non-Docker Hub registries, include the registry address as well (e.g., ghcr.io/namespace or quay.io/namespace).").String()
dockerScanRegistryToken = dockerScan.Flag("registry-token", "Optional Docker registry access token. Provide this if you want to include private images within the specified namespace.").String()
travisCiScan = cli.Command("travisci", "Scan TravisCI")
travisCiScanToken = travisCiScan.Flag("token", "TravisCI token. Can also be provided with environment variable").Envar("TRAVISCI_TOKEN").Required().String()
// Postman is hidden for now until we get more feedback from the community.
postmanScan = cli.Command("postman", "Scan Postman")
postmanToken = postmanScan.Flag("token", "Postman token. Can also be provided with environment variable").Envar("POSTMAN_TOKEN").String()
postmanWorkspaces = postmanScan.Flag("workspace", "Postman workspace to scan. You can repeat this flag. Deprecated flag.").Hidden().Strings()
postmanWorkspaceIDs = postmanScan.Flag("workspace-id", "Postman workspace ID to scan. You can repeat this flag.").Strings()
postmanCollections = postmanScan.Flag("collection", "Postman collection to scan. You can repeat this flag. Deprecated flag.").Hidden().Strings()
postmanCollectionIDs = postmanScan.Flag("collection-id", "Postman collection ID to scan. You can repeat this flag.").Strings()
postmanEnvironments = postmanScan.Flag("environment", "Postman environment to scan. You can repeat this flag.").Strings()
postmanIncludeCollections = postmanScan.Flag("include-collections", "Collections to include in scan. You can repeat this flag. Deprecated flag.").Hidden().Strings()
postmanIncludeCollectionIDs = postmanScan.Flag("include-collection-id", "Collection ID to include in scan. You can repeat this flag.").Strings()
postmanIncludeEnvironments = postmanScan.Flag("include-environments", "Environments to include in scan. You can repeat this flag.").Strings()
postmanExcludeCollections = postmanScan.Flag("exclude-collections", "Collections to exclude from scan. You can repeat this flag. Deprecated flag.").Hidden().Strings()
postmanExcludeCollectionIDs = postmanScan.Flag("exclude-collection-id", "Collection ID to exclude from scan. You can repeat this flag.").Strings()
postmanExcludeEnvironments = postmanScan.Flag("exclude-environments", "Environments to exclude from scan. You can repeat this flag.").Strings()
postmanWorkspacePaths = postmanScan.Flag("workspace-paths", "Path to Postman workspaces.").Strings()
postmanCollectionPaths = postmanScan.Flag("collection-paths", "Path to Postman collections.").Strings()
postmanEnvironmentPaths = postmanScan.Flag("environment-paths", "Path to Postman environments.").Strings()
elasticsearchScan = cli.Command("elasticsearch", "Scan Elasticsearch")
elasticsearchNodes = elasticsearchScan.Flag("nodes", "Elasticsearch nodes").Envar("ELASTICSEARCH_NODES").Strings()
elasticsearchUsername = elasticsearchScan.Flag("username", "Elasticsearch username").Envar("ELASTICSEARCH_USERNAME").String()
elasticsearchPassword = elasticsearchScan.Flag("password", "Elasticsearch password").Envar("ELASTICSEARCH_PASSWORD").String()
elasticsearchServiceToken = elasticsearchScan.Flag("service-token", "Elasticsearch service token").Envar("ELASTICSEARCH_SERVICE_TOKEN").String()
elasticsearchCloudId = elasticsearchScan.Flag("cloud-id", "Elasticsearch cloud ID. Can also be provided with environment variable").Envar("ELASTICSEARCH_CLOUD_ID").String()
elasticsearchAPIKey = elasticsearchScan.Flag("api-key", "Elasticsearch API key. Can also be provided with environment variable").Envar("ELASTICSEARCH_API_KEY").String()
elasticsearchIndexPattern = elasticsearchScan.Flag("index-pattern", "Filters the indices to search").Default("*").Envar("ELASTICSEARCH_INDEX_PATTERN").String()
elasticsearchQueryJSON = elasticsearchScan.Flag("query-json", "Filters the documents to search").Envar("ELASTICSEARCH_QUERY_JSON").String()
elasticsearchSinceTimestamp = elasticsearchScan.Flag("since-timestamp", "Filters the documents to search to those created since this timestamp; overrides any timestamp from --query-json").Envar("ELASTICSEARCH_SINCE_TIMESTAMP").String()
elasticsearchBestEffortScan = elasticsearchScan.Flag("best-effort-scan", "Attempts to continuously scan a cluster").Envar("ELASTICSEARCH_BEST_EFFORT_SCAN").Bool()
jenkinsScan = cli.Command("jenkins", "Scan Jenkins")
jenkinsURL = jenkinsScan.Flag("url", "Jenkins URL").Envar("JENKINS_URL").Required().String()
jenkinsUsername = jenkinsScan.Flag("username", "Jenkins username").Envar("JENKINS_USERNAME").String()
jenkinsPassword = jenkinsScan.Flag("password", "Jenkins password").Envar("JENKINS_PASSWORD").String()
jenkinsInsecureSkipVerifyTLS = jenkinsScan.Flag("insecure-skip-verify-tls", "Skip TLS verification").Envar("JENKINS_INSECURE_SKIP_VERIFY_TLS").Bool()
huggingfaceScan = cli.Command("huggingface", "Find credentials in HuggingFace datasets, models and spaces.")
huggingfaceEndpoint = huggingfaceScan.Flag("endpoint", "HuggingFace endpoint.").Default("https://huggingface.co").String()
huggingfaceModels = huggingfaceScan.Flag("model", "HuggingFace model to scan. You can repeat this flag. Example: 'username/model'").Strings()
huggingfaceSpaces = huggingfaceScan.Flag("space", "HuggingFace space to scan. You can repeat this flag. Example: 'username/space'").Strings()
huggingfaceDatasets = huggingfaceScan.Flag("dataset", "HuggingFace dataset to scan. You can repeat this flag. Example: 'username/dataset'").Strings()
huggingfaceOrgs = huggingfaceScan.Flag("org", `HuggingFace organization to scan. You can repeat this flag. Example: "trufflesecurity"`).Strings()
huggingfaceUsers = huggingfaceScan.Flag("user", `HuggingFace user to scan. You can repeat this flag. Example: "trufflesecurity"`).Strings()
huggingfaceToken = huggingfaceScan.Flag("token", "HuggingFace token. Can be provided with environment variable HUGGINGFACE_TOKEN.").Envar("HUGGINGFACE_TOKEN").String()
huggingfaceIncludeModels = huggingfaceScan.Flag("include-models", "Models to include in scan. You can repeat this flag. Must use HuggingFace model full name. Example: 'username/model' (Only used with --user or --org)").Strings()
huggingfaceIncludeSpaces = huggingfaceScan.Flag("include-spaces", "Spaces to include in scan. You can repeat this flag. Must use HuggingFace space full name. Example: 'username/space' (Only used with --user or --org)").Strings()
huggingfaceIncludeDatasets = huggingfaceScan.Flag("include-datasets", "Datasets to include in scan. You can repeat this flag. Must use HuggingFace dataset full name. Example: 'username/dataset' (Only used with --user or --org)").Strings()
huggingfaceIgnoreModels = huggingfaceScan.Flag("ignore-models", "Models to ignore in scan. You can repeat this flag. Must use HuggingFace model full name. Example: 'username/model' (Only used with --user or --org)").Strings()
huggingfaceIgnoreSpaces = huggingfaceScan.Flag("ignore-spaces", "Spaces to ignore in scan. You can repeat this flag. Must use HuggingFace space full name. Example: 'username/space' (Only used with --user or --org)").Strings()
huggingfaceIgnoreDatasets = huggingfaceScan.Flag("ignore-datasets", "Datasets to ignore in scan. You can repeat this flag. Must use HuggingFace dataset full name. Example: 'username/dataset' (Only used with --user or --org)").Strings()
huggingfaceSkipAllModels = huggingfaceScan.Flag("skip-all-models", "Skip all model scans. (Only used with --user or --org)").Bool()
huggingfaceSkipAllSpaces = huggingfaceScan.Flag("skip-all-spaces", "Skip all space scans. (Only used with --user or --org)").Bool()
huggingfaceSkipAllDatasets = huggingfaceScan.Flag("skip-all-datasets", "Skip all dataset scans. (Only used with --user or --org)").Bool()
huggingfaceIncludeDiscussions = huggingfaceScan.Flag("include-discussions", "Include discussions in scan.").Bool()
huggingfaceIncludePrs = huggingfaceScan.Flag("include-prs", "Include pull requests in scan.").Bool()
stdinInputScan = cli.Command("stdin", "Find credentials from stdin.")
multiScanScan = cli.Command("multi-scan", "Find credentials in multiple sources defined in configuration.")
jsonEnumeratorScan = cli.Command("json-enumerator", "Find credentials from a JSON enumerator input.")
jsonEnumeratorPaths = jsonEnumeratorScan.Arg("path", "Path to JSON enumerator file to scan.").Strings()
analyzeCmd = analyzer.Command(cli)
usingTUI = false
)
func init() {
_, _ = maxprocs.Set()
for i, arg := range os.Args {
if strings.HasPrefix(arg, "--") {
split := strings.SplitN(arg, "=", 2)
split[0] = strings.ReplaceAll(split[0], "_", "-")
os.Args[i] = strings.Join(split, "=")
}
}
cli.Version("trufflehog " + version.BuildVersion)
// Support -h for help and write it to stdout.
cli.HelpFlag.Short('h')
cli.UsageWriter(os.Stdout)
// Check if the TUI environment variable is set.
if ok, err := strconv.ParseBool(os.Getenv("TUI_PARENT")); err == nil {
usingTUI = ok
}
if isatty.IsTerminal(os.Stdout.Fd()) && (len(os.Args) <= 1 || os.Args[1] == analyzeCmd.FullCommand()) {
args := tui.Run(os.Args[1:])
if len(args) == 0 {
os.Exit(0)
}
binary, err := exec.LookPath("sh")
if err == nil {
// On success, this call will never return. On failure, fallthrough
// to overwriting os.Args.
cmd := strings.Join(append(os.Args[:1], args...), " ")
_ = syscall.Exec(binary, []string{"sh", "-c", cmd}, append(os.Environ(), "TUI_PARENT=true"))
}
// Overwrite the Args slice so overseer works properly.
os.Args = os.Args[:1]
os.Args = append(os.Args, args...)
usingTUI = true
}
cmd = kingpin.MustParse(cli.Parse(os.Args[1:]))
// Configure logging.
switch {
case *trace:
log.SetLevel(5)
case *debug:
log.SetLevel(2)
default:
l := int8(*logLevel)
if l < -1 || l > 5 {
fmt.Fprintf(os.Stderr, "invalid log level: %d\n", *logLevel)
os.Exit(1)
}
if l == -1 {
// Zap uses "5" as the value for fatal.
// We need to pass in "-5" because `SetLevel` passes the negation.
log.SetLevel(-5)
} else {
log.SetLevel(l)
}
}
if *noColor || *noColour {
color.NoColor = true // disables colorized output
}
}
// syncLogs flushes logs when the program exits.
func syncLogs(syncFn func() error) {
if syncFn != nil {
_ = syncFn()
}
}
func main() {
// setup logger
logFormat := log.WithConsoleSink
if *jsonOut {
logFormat = log.WithJSONSink
}
logger, sync := log.New("trufflehog", logFormat(os.Stderr, log.WithGlobalRedaction()))
// make it the default logger for contexts
context.SetDefaultLogger(logger)
if *localDev {
run(overseer.State{}, sync)
os.Exit(0)
}
logFatal := logFatalFunc(logger, sync)
updateCfg := overseer.Config{
Program: func(s overseer.State) {
run(s, sync)
},
Debug: *debug,
RestartSignal: syscall.SIGTERM,
// TODO: Eventually add a PreUpgrade func for signature check w/ x509 PKCS1v15
// PreUpgrade: checkUpdateSignature(binaryPath string),
}
if !*noUpdate {
topLevelCmd, _, _ := strings.Cut(cmd, " ")
updateCfg.Fetcher = updater.Fetcher(topLevelCmd, usingTUI)
}
if version.BuildVersion == "dev" {
updateCfg.Fetcher = nil
}
err := overseer.RunErr(updateCfg)
if err != nil {
logFatal(err, "error occurred with trufflehog updater 🐷")
}
}
func run(state overseer.State, logSync func() error) {
ctx, cancel := context.WithCancelCause(context.Background())
defer cancel(nil)
defer syncLogs(logSync)
go func() {
if err := cleantemp.CleanTempArtifacts(ctx); err != nil {
ctx.Logger().Error(err, "error cleaning temporary artifacts")
}
}()
logger := ctx.Logger()
logFatal := logFatalFunc(logger, logSync)
killSignal := make(chan os.Signal, 1)
signal.Notify(killSignal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
<-killSignal
logger.Info("Received signal, shutting down.")
cancel(fmt.Errorf("canceling context due to signal"))
if err := cleantemp.CleanTempArtifacts(ctx); err != nil {
logger.Error(err, "error cleaning temporary artifacts")
} else {
logger.Info("cleaned temporary artifacts")
}
syncLogs(logSync)
os.Exit(0)
}()
logger.V(2).Info(fmt.Sprintf("trufflehog %s", version.BuildVersion))
if *githubScanToken != "" {
// NOTE: this kludge is here to do an authenticated shallow commit
// TODO: refactor to better pass credentials
os.Setenv("GITHUB_TOKEN", *githubScanToken)
}
// When setting a base commit, chunks must be scanned in order.
if *gitScanSinceCommit != "" {
*concurrency = 1
}
if *profile {
runtime.SetBlockProfileRate(1)
runtime.SetMutexProfileFraction(-1)
go func() {
router := http.NewServeMux()
router.Handle("/debug/pprof/", http.DefaultServeMux)
router.Handle("/debug/fgprof", fgprof.Handler())
logger.Info("starting pprof and fgprof server on :18066 /debug/pprof and /debug/fgprof")
if err := http.ListenAndServe(":18066", router); err != nil {
logger.Error(err, "error serving pprof and fgprof")
}
}()
}
// Set feature configurations from CLI flags
if *forceSkipBinaries {
feature.ForceSkipBinaries.Store(true)
}
if *forceSkipArchives {
feature.ForceSkipArchives.Store(true)
}
if gitCloneTimeout != nil {
feature.GitCloneTimeoutDuration.Store(int64(*gitCloneTimeout))
}
if *skipAdditionalRefs {
feature.SkipAdditionalRefs.Store(true)
}
if *userAgentSuffix != "" {
feature.UserAgentSuffix.Store(*userAgentSuffix)
}
// OSS Default APK handling on
feature.EnableAPKHandler.Store(true)
// OSS Default Use Git Mirror on
feature.UseGitMirror.Store(true)
// OSS Default simplified gitlab enumeration
feature.UseSimplifiedGitlabEnumeration.Store(true)
feature.GitlabProjectsPerPage.Store(100)
// OSS Default using github graphql api for issues, pr's and comments
feature.UseGithubGraphQLAPI.Store(false)
conf := &config.Config{}
if *configFilename != "" {
var err error
conf, err = config.Read(*configFilename)
if err != nil {
logFatal(err, "error parsing the provided configuration file")
}
}
if *detectorTimeout != 0 {
logger.Info("Setting detector timeout", "timeout", detectorTimeout.String())
engine.SetDetectorTimeout(*detectorTimeout)
detectors.OverrideDetectorTimeout(*detectorTimeout)
}
if *archiveMaxSize != 0 {
handlers.SetArchiveMaxSize(int(*archiveMaxSize))
}
if *archiveMaxDepth != 0 {
handlers.SetArchiveMaxDepth(*archiveMaxDepth)
}
if *archiveTimeout != 0 {
handlers.SetArchiveMaxTimeout(*archiveTimeout)
}
// Set how the engine will print its results.
var printer engine.Printer
switch {
case *jsonLegacy:
printer = new(output.LegacyJSONPrinter)
case *jsonOut:
printer = new(output.JSONPrinter)
case *gitHubActionsFormat:
printer = new(output.GitHubActionsPrinter)
default:
printer = new(output.PlainPrinter)
}
if !*jsonLegacy && !*jsonOut {
fmt.Fprintf(os.Stderr, "🐷🔑🐷 TruffleHog. Unearth your secrets. 🐷🔑🐷\n\n")
}
// Parse --results flag.
if *onlyVerified {
r := "verified"
results = &r
}
parsedResults, err := parseResults(results)
if err != nil {
logFatal(err, "failed to configure results flag")
}
verificationCacheMetrics := verificationcache.InMemoryMetrics{}
engConf := engine.Config{
Concurrency: *concurrency,
ConfiguredSources: conf.Sources,
// The engine must always be configured with the list of
// default detectors, which can be further filtered by the
// user. The filters are applied by the engine and are only
// subtractive.
Detectors: append(defaults.DefaultDetectors(), conf.Detectors...),
Verify: !*noVerification,
IncludeDetectors: *includeDetectors,
ExcludeDetectors: *excludeDetectors,
CustomVerifiersOnly: *customVerifiersOnly,
VerifierEndpoints: *verifiers,
Dispatcher: engine.NewPrinterDispatcher(printer),
FilterUnverified: *filterUnverified,
FilterEntropy: *filterEntropy,
VerificationOverlap: *allowVerificationOverlap,
Results: parsedResults,
PrintAvgDetectorTime: *printAvgDetectorTime,
ShouldScanEntireChunk: *scanEntireChunk,
MaxDecodeDepth: *maxDecodeDepth,
VerificationCacheMetrics: &verificationCacheMetrics,
}
if !*noVerificationCache {
engConf.VerificationResultCache = simple.NewCache[detectors.Result]()
}
// Check that there are no sources defined for non-scan subcommands. If
// there are, return an error as it is ambiguous what the user is
// trying to do.
if cmd != multiScanScan.FullCommand() && len(conf.Sources) > 0 {
logFatal(
fmt.Errorf("ambiguous configuration"),
"sources should only be defined in configuration for the 'multi-scan' command",
)
}
if *compareDetectionStrategies {
if err := compareScans(ctx, cmd, engConf); err != nil {
logFatal(err, "error comparing detection strategies")
}
return
}
metrics, err := runSingleScan(ctx, cmd, engConf)
if err != nil {
logFatal(err, "error running scan")
}
verificationCacheMetricsSnapshot := struct {
Hits int32
Misses int32
HitsWasted int32
AttemptsSaved int32
VerificationTimeSpentMS int64
}{
Hits: verificationCacheMetrics.ResultCacheHits.Load(),
Misses: verificationCacheMetrics.ResultCacheMisses.Load(),
HitsWasted: verificationCacheMetrics.ResultCacheHitsWasted.Load(),
AttemptsSaved: verificationCacheMetrics.CredentialVerificationsSaved.Load(),
VerificationTimeSpentMS: verificationCacheMetrics.FromDataVerifyTimeSpentMS.Load(),
}
// Print results.
logger.Info("finished scanning",
"chunks", metrics.ChunksScanned,
"bytes", metrics.BytesScanned,
"verified_secrets", metrics.VerifiedSecretsFound,
"unverified_secrets", metrics.UnverifiedSecretsFound,
"scan_duration", metrics.ScanDuration.String(),
"trufflehog_version", version.BuildVersion,
"verification_caching", verificationCacheMetricsSnapshot,
)
if metrics.hasFoundResults && *fail {
logger.V(2).Info("exiting with code 183 because results were found")
syncLogs(logSync)
os.Exit(183)
}
}
func compareScans(ctx context.Context, cmd string, cfg engine.Config) error {
var (
entireMetrics metrics
maxLengthMetrics metrics
err error
)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// Run scan with entire chunk span calculator.
cfg.ShouldScanEntireChunk = true
entireMetrics, err = runSingleScan(ctx, cmd, cfg)
if err != nil {
ctx.Logger().Error(err, "error running scan with entire chunk span calculator")
}
}()
// Run scan with max-length span calculator.
maxLengthMetrics, err = runSingleScan(ctx, cmd, cfg)
if err != nil {
return fmt.Errorf("error running scan with custom span calculator: %v", err)
}
wg.Wait()
return compareMetrics(maxLengthMetrics.Metrics, entireMetrics.Metrics)
}
func compareMetrics(customMetrics, entireMetrics engine.Metrics) error {
fmt.Printf("Comparison of scan results: \n")
fmt.Printf("Custom span - Chunks: %d, Bytes: %d, Verified Secrets: %d, Unverified Secrets: %d, Duration: %s\n",
customMetrics.ChunksScanned, customMetrics.BytesScanned, customMetrics.VerifiedSecretsFound, customMetrics.UnverifiedSecretsFound, customMetrics.ScanDuration.String())
fmt.Printf("Entire chunk - Chunks: %d, Bytes: %d, Verified Secrets: %d, Unverified Secrets: %d, Duration: %s\n",
entireMetrics.ChunksScanned, entireMetrics.BytesScanned, entireMetrics.VerifiedSecretsFound, entireMetrics.UnverifiedSecretsFound, entireMetrics.ScanDuration.String())
// Check for differences in scan metrics.
if customMetrics.ChunksScanned != entireMetrics.ChunksScanned ||
customMetrics.BytesScanned != entireMetrics.BytesScanned ||
customMetrics.VerifiedSecretsFound != entireMetrics.VerifiedSecretsFound {
return fmt.Errorf("scan metrics do not match")
}
return nil
}
type metrics struct {
engine.Metrics
hasFoundResults bool
}
func runSingleScan(ctx context.Context, cmd string, cfg engine.Config) (metrics, error) {
var scanMetrics metrics
// Setup job report writer if provided
var jobReportWriter io.WriteCloser
if *jobReportFile != nil {
jobReportWriter = *jobReportFile
}
handleFinishedMetrics := func(ctx context.Context, finishedMetrics <-chan sources.UnitMetrics, jobReportWriter io.WriteCloser) {
go func() {
defer func() {
jobReportWriter.Close()
if namer, ok := jobReportWriter.(interface{ Name() string }); ok {
ctx.Logger().Info("report written", "path", namer.Name())
} else {
ctx.Logger().Info("report written")
}
}()
for metrics := range finishedMetrics {
metrics.Errors = common.ExportErrors(metrics.Errors...)
details, err := json.Marshal(map[string]any{
"version": 1,
"data": metrics,
})
if err != nil {
ctx.Logger().Error(err, "error marshalling job details")
continue
}
if _, err := jobReportWriter.Write(append(details, '\n')); err != nil {
ctx.Logger().Error(err, "error writing to file")
}
}
}()
}
const defaultOutputBufferSize = 64
opts := []func(*sources.SourceManager){
sources.WithConcurrentSources(cfg.Concurrency),
sources.WithConcurrentUnits(cfg.Concurrency),
sources.WithSourceUnits(),
sources.WithBufferedOutput(defaultOutputBufferSize),
}
if jobReportWriter != nil {
unitHook, finishedMetrics := sources.NewUnitHook(ctx)
opts = append(opts, sources.WithReportHook(unitHook))
handleFinishedMetrics(ctx, finishedMetrics, jobReportWriter)
}
cfg.SourceManager = sources.NewManager(opts...)
eng, err := engine.NewEngine(ctx, &cfg)
if err != nil {
return scanMetrics, fmt.Errorf("error initializing engine: %v", err)
}
eng.Start(ctx)
persistGitRepo := *gitNoCleanup || *githubNoCleanup || *gitlabNoCleanup
gitCloneTempPath := ""
defer func() {
// Clean up temporary artifacts.
if err := cleantemp.CleanTempArtifacts(ctx); err != nil {
ctx.Logger().Error(err, "error cleaning temp artifacts")
}
if *jsonLegacy {
// If JSON legacy is enabled, that means the cloned repos are not deleted yet
// because they were needed for outputting legacy JSON.
// We only clean them up here if the user did not request to persist them.
if !persistGitRepo {
if err := cleantemp.CleanTempDirsForLegacyJSON(gitCloneTempPath); err != nil {
ctx.Logger().Error(err, "error cleaning temp artifacts for legacy JSON")
}
}
}
}()
var refs []sources.JobProgressRef
switch cmd {
case gitScan.FullCommand():
if err := validateClonePath(*gitClonePath, *gitNoCleanup); err != nil {
return scanMetrics, err
}
gitCfg := sources.GitConfig{
URI: *gitScanURI,
IncludePathsFile: *gitScanIncludePaths,
ExcludePathsFile: *gitScanExcludePaths,
HeadRef: *gitScanBranch,
BaseRef: *gitScanSinceCommit,
MaxDepth: *gitScanMaxDepth,
Bare: *gitScanBare,
ExcludeGlobs: *gitScanExcludeGlobs,
ClonePath: *gitClonePath,
NoCleanup: *gitNoCleanup,
PrintLegacyJSON: *jsonLegacy,
TrustLocalGitConfig: *gitTrustLocalGitConfig,
}
// detect if trufflehog is running git source as a pre-commit hook
if isPreCommitHook() {
ctx.Logger().Info("Running as a pre-commit hook, overriding default flags for hook context")
// Override git configuration for pre-commit hook context
gitCfg.TrustLocalGitConfig = true
gitCfg.BaseRef = "HEAD" // Only scan staged changes
// Override result filters for pre-commit hook context
// In hook mode, we only want to show verified secrets and unknown findings
*results = "verified,unknown"
// Override failure behavior for pre-commit hook context
// In hook mode, we want to fail the commit if any secrets are found
*fail = true
}
if ref, err := eng.ScanGit(ctx, gitCfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan Git: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case githubScan.FullCommand():
gitCloneTempPath = *githubClonePath
filter, err := common.FilterFromFiles(*githubScanIncludePaths, *githubScanExcludePaths)
if err != nil {
return scanMetrics, fmt.Errorf("could not create filter: %v", err)
}
if len(*githubScanOrgs) == 0 && len(*githubScanRepos) == 0 {
return scanMetrics, fmt.Errorf("invalid config: you must specify at least one organization or repository")
}
if len(*githubScanOrgs) > 0 && len(*githubScanRepos) > 0 {
return scanMetrics, fmt.Errorf("invalid config: you cannot specify both organizations and repositories at the same time")
}
if err := validateClonePath(*githubClonePath, *githubNoCleanup); err != nil {
return scanMetrics, err
}
cfg := sources.GithubConfig{
Endpoint: *githubScanEndpoint,
Token: *githubScanToken,
IncludeForks: *githubIncludeForks,
IncludeMembers: *githubIncludeMembers,
IncludeWikis: *githubIncludeWikis,
Concurrency: *concurrency,
ExcludeRepos: *githubExcludeRepos,
IncludeRepos: *githubIncludeRepos,
Repos: *githubScanRepos,
Orgs: *githubScanOrgs,
IncludeIssueComments: *githubScanIssueComments,
IncludePullRequestComments: *githubScanPRComments,
IncludeGistComments: *githubScanGistComments,
CommentsTimeframeDays: *githubCommentsTimeframeDays,
Filter: filter,
AuthInUrl: *githubAuthInUrl,
ClonePath: *githubClonePath,
NoCleanup: *githubNoCleanup,
IgnoreGists: *githubIgnoreGists,
PrintLegacyJSON: *jsonLegacy,
}
if ref, err := eng.ScanGitHub(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan Github: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case githubExperimentalScan.FullCommand():
cfg := sources.GitHubExperimentalConfig{
Token: *githubExperimentalToken,
Repository: *githubExperimentalRepo,
ObjectDiscovery: *githubExperimentalObjectDiscovery,
CollisionThreshold: *githubExperimentalCollisionThreshold,
DeleteCachedData: *githubExperimentalDeleteCache,
}
if ref, err := eng.ScanGitHubExperimental(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan using Github Experimental: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case gitlabScan.FullCommand():
gitCloneTempPath = *gitlabClonePath
filter, err := common.FilterFromFiles(*gitlabScanIncludePaths, *gitlabScanExcludePaths)
if err != nil {
return scanMetrics, fmt.Errorf("could not create filter: %v", err)
}
if len(*gitlabScanRepos) > 0 && len(*gitlabScanGroupIds) > 0 {
return scanMetrics, fmt.Errorf("invalid config: you cannot specify both repositories and groups at the same time")
}
if err := validateClonePath(*gitlabClonePath, *gitlabNoCleanup); err != nil {
return scanMetrics, err
}
cfg := sources.GitlabConfig{
Endpoint: *gitlabScanEndpoint,
Token: *gitlabScanToken,
Repos: *gitlabScanRepos,
GroupIds: *gitlabScanGroupIds,
IncludeRepos: *gitlabScanIncludeRepos,
ExcludeRepos: *gitlabScanExcludeRepos,
Filter: filter,
AuthInUrl: *gitlabAuthInUrl,
ClonePath: *gitlabClonePath,
NoCleanup: *gitlabNoCleanup,
PrintLegacyJSON: *jsonLegacy,
}
if ref, err := eng.ScanGitLab(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan GitLab: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case filesystemScan.FullCommand():
if len(*filesystemDirectories) > 0 {
ctx.Logger().Info("--directory flag is deprecated, please pass directories as arguments")
}
paths := make([]string, 0, len(*filesystemPaths)+len(*filesystemDirectories))
paths = append(paths, *filesystemPaths...)
paths = append(paths, *filesystemDirectories...)
cfg := sources.FilesystemConfig{
Paths: paths,
IncludePathsFile: *filesystemScanIncludePaths,
ExcludePathsFile: *filesystemScanExcludePaths,
MaxSymlinkDepth: *filesystemScanMaxSymlinkDepth,
}
if ref, err := eng.ScanFileSystem(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan filesystem: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case s3Scan.FullCommand():
cfg := sources.S3Config{
Key: *s3ScanKey,
Secret: *s3ScanSecret,
SessionToken: *s3ScanSessionToken,
Buckets: *s3ScanBuckets,
IgnoreBuckets: *s3ScanIgnoreBuckets,
Roles: *s3ScanRoleArns,
CloudCred: *s3ScanCloudEnv,
MaxObjectSize: int64(*s3ScanMaxObjectSize),
}
if ref, err := eng.ScanS3(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan S3: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case syslogScan.FullCommand():
cfg := sources.SyslogConfig{
Address: *syslogAddress,
Format: *syslogFormat,
Protocol: *syslogProtocol,
CertPath: *syslogTLSCert,
KeyPath: *syslogTLSKey,
Concurrency: *concurrency,
}
if ref, err := eng.ScanSyslog(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan syslog: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case circleCiScan.FullCommand():
if ref, err := eng.ScanCircleCI(ctx, *circleCiScanToken); err != nil {
return scanMetrics, fmt.Errorf("failed to scan CircleCI: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case travisCiScan.FullCommand():
if ref, err := eng.ScanTravisCI(ctx, *travisCiScanToken); err != nil {
return scanMetrics, fmt.Errorf("failed to scan TravisCI: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case gcsScan.FullCommand():
cfg := sources.GCSConfig{
ProjectID: *gcsProjectID,
CloudCred: *gcsCloudEnv,
ServiceAccount: *gcsServiceAccount,
WithoutAuth: *gcsWithoutAuth,
ApiKey: *gcsAPIKey,
IncludeBuckets: commaSeparatedToSlice(*gcsIncludeBuckets),
ExcludeBuckets: commaSeparatedToSlice(*gcsExcludeBuckets),
IncludeObjects: commaSeparatedToSlice(*gcsIncludeObjects),
ExcludeObjects: commaSeparatedToSlice(*gcsExcludeObjects),
Concurrency: *concurrency,
MaxObjectSize: int64(*gcsMaxObjectSize),
}
if ref, err := eng.ScanGCS(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan GCS: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case dockerScan.FullCommand():
if *dockerScanImages != nil && *dockerScanNamespace != "" {
return scanMetrics, fmt.Errorf("invalid config: you cannot specify both images and namespace at the same time")
}
if *dockerScanImages == nil && *dockerScanNamespace == "" {
return scanMetrics, fmt.Errorf("invalid config: both images and namespace cannot be empty; one is required")
}
if *dockerScanRegistryToken != "" && *dockerScanNamespace == "" {
return scanMetrics, fmt.Errorf("invalid config: registry token can only be used with registry namespace")
}
cfg := sources.DockerConfig{
BearerToken: *dockerScanToken,
Images: *dockerScanImages,
UseDockerKeychain: *dockerScanToken == "",
ExcludePaths: strings.Split(*dockerExcludePaths, ","),
Namespace: *dockerScanNamespace,
RegistryToken: *dockerScanRegistryToken,
}
if ref, err := eng.ScanDocker(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan Docker: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case postmanScan.FullCommand():
// handle deprecated flag
workspaceIDs := make([]string, 0, len(*postmanWorkspaceIDs)+len(*postmanWorkspaces))
workspaceIDs = append(workspaceIDs, *postmanWorkspaceIDs...)
workspaceIDs = append(workspaceIDs, *postmanWorkspaces...)
// handle deprecated flag
collectionIDs := make([]string, 0, len(*postmanCollectionIDs)+len(*postmanCollections))
collectionIDs = append(collectionIDs, *postmanCollectionIDs...)
collectionIDs = append(collectionIDs, *postmanCollections...)
// handle deprecated flag
includeCollectionIDs := make([]string, 0, len(*postmanIncludeCollectionIDs)+len(*postmanIncludeCollections))
includeCollectionIDs = append(includeCollectionIDs, *postmanIncludeCollectionIDs...)
includeCollectionIDs = append(includeCollectionIDs, *postmanIncludeCollections...)
// handle deprecated flag
excludeCollectionIDs := make([]string, 0, len(*postmanExcludeCollectionIDs)+len(*postmanExcludeCollections))
excludeCollectionIDs = append(excludeCollectionIDs, *postmanExcludeCollectionIDs...)
excludeCollectionIDs = append(excludeCollectionIDs, *postmanExcludeCollections...)
cfg := sources.PostmanConfig{
Token: *postmanToken,
Workspaces: workspaceIDs,
Collections: collectionIDs,
Environments: *postmanEnvironments,
IncludeCollections: includeCollectionIDs,
IncludeEnvironments: *postmanIncludeEnvironments,
ExcludeCollections: excludeCollectionIDs,
ExcludeEnvironments: *postmanExcludeEnvironments,
CollectionPaths: *postmanCollectionPaths,
WorkspacePaths: *postmanWorkspacePaths,
EnvironmentPaths: *postmanEnvironmentPaths,
}
if ref, err := eng.ScanPostman(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan Postman: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case elasticsearchScan.FullCommand():
cfg := sources.ElasticsearchConfig{
Nodes: *elasticsearchNodes,
Username: *elasticsearchUsername,
Password: *elasticsearchPassword,
CloudID: *elasticsearchCloudId,
APIKey: *elasticsearchAPIKey,
ServiceToken: *elasticsearchServiceToken,
IndexPattern: *elasticsearchIndexPattern,
QueryJSON: *elasticsearchQueryJSON,
SinceTimestamp: *elasticsearchSinceTimestamp,
BestEffortScan: *elasticsearchBestEffortScan,
}
if ref, err := eng.ScanElasticsearch(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan Elasticsearch: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case jenkinsScan.FullCommand():
cfg := engine.JenkinsConfig{
Endpoint: *jenkinsURL,
InsecureSkipVerifyTLS: *jenkinsInsecureSkipVerifyTLS,
Username: *jenkinsUsername,
Password: *jenkinsPassword,
}
if ref, err := eng.ScanJenkins(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan Jenkins: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case huggingfaceScan.FullCommand():
if *huggingfaceEndpoint != "" {
*huggingfaceEndpoint = strings.TrimRight(*huggingfaceEndpoint, "/")
}
if len(*huggingfaceModels) == 0 && len(*huggingfaceSpaces) == 0 && len(*huggingfaceDatasets) == 0 && len(*huggingfaceOrgs) == 0 && len(*huggingfaceUsers) == 0 {
return scanMetrics, fmt.Errorf("invalid config: you must specify at least one organization, user, model, space or dataset")
}
cfg := engine.HuggingfaceConfig{
Endpoint: *huggingfaceEndpoint,
Models: *huggingfaceModels,
Spaces: *huggingfaceSpaces,
Datasets: *huggingfaceDatasets,
Organizations: *huggingfaceOrgs,
Users: *huggingfaceUsers,
Token: *huggingfaceToken,
IncludeModels: *huggingfaceIncludeModels,
IncludeSpaces: *huggingfaceIncludeSpaces,
IncludeDatasets: *huggingfaceIncludeDatasets,
IgnoreModels: *huggingfaceIgnoreModels,
IgnoreSpaces: *huggingfaceIgnoreSpaces,
IgnoreDatasets: *huggingfaceIgnoreDatasets,
SkipAllModels: *huggingfaceSkipAllModels,
SkipAllSpaces: *huggingfaceSkipAllSpaces,
SkipAllDatasets: *huggingfaceSkipAllDatasets,
IncludeDiscussions: *huggingfaceIncludeDiscussions,
IncludePrs: *huggingfaceIncludePrs,
Concurrency: *concurrency,
}
if ref, err := eng.ScanHuggingface(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan HuggingFace: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case multiScanScan.FullCommand():
if *configFilename == "" {
return scanMetrics, fmt.Errorf("missing required flag: --config")
}
if rs, err := eng.ScanConfig(ctx, cfg.ConfiguredSources...); err != nil {
return scanMetrics, fmt.Errorf("failed to scan via config: %w", err)
} else {
refs = rs
}
case stdinInputScan.FullCommand():
cfg := sources.StdinConfig{}
if ref, err := eng.ScanStdinInput(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan stdin input: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
case jsonEnumeratorScan.FullCommand():
cfg := sources.JSONEnumeratorConfig{Paths: *jsonEnumeratorPaths}
if ref, err := eng.ScanJSONEnumeratorInput(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan JSON enumerator input: %v", err)
} else {
refs = []sources.JobProgressRef{ref}
}
default:
return scanMetrics, fmt.Errorf("invalid command: %s", cmd)
}
// Wait for all workers to finish.
if err = eng.Finish(ctx); err != nil {
return scanMetrics, fmt.Errorf("engine failed to finish execution: %v", err)
}
// Print any non-fatal errors reported during the scan.
var retErr error
for _, ref := range refs {
if errs := ref.Snapshot().Errors; len(errs) > 0 {
if *failOnScanErrors {
retErr = fmt.Errorf("encountered errors during scan")
}
errMsgs := make([]string, len(errs))
for i := 0; i < len(errs); i++ {
errMsgs[i] = errs[i].Error()
}
ctx.Logger().Error(nil, "encountered errors during scan",
"job", ref.JobID,
"source_name", ref.SourceName,
"errors", errMsgs,
)
}
}
if *printAvgDetectorTime {
printAverageDetectorTime(eng)
}
return metrics{Metrics: eng.GetMetrics(), hasFoundResults: eng.HasFoundResults()}, retErr
}
// parseResults ensures that users provide valid CSV input to `--results`.
//
// This is a work-around to kingpin not supporting CSVs.
// See: https://github.com/trufflesecurity/trufflehog/pull/2372#issuecomment-1983868917
func parseResults(input *string) (map[string]struct{}, error) {
if *input == "" {
return nil, nil
}
var (
values = strings.Split(strings.ToLower(*input), ",")
results = make(map[string]struct{}, 3)
)
for _, value := range values {
switch value {
case "verified", "unknown", "unverified", "filtered_unverified":
results[value] = struct{}{}
default:
return nil, fmt.Errorf("invalid value '%s', valid values are 'verified,unknown,unverified,filtered_unverified'", value)
}
}
return results, nil
}
// logFatalFunc returns a log.Fatal style function. Calling the returned
// function will terminate the program without cleanup.
func logFatalFunc(logger logr.Logger, logSync func() error) func(error, string, ...any) {
return func(err error, message string, keyAndVals ...any) {
logger.Error(err, message, keyAndVals...)
syncLogs(logSync)
if err != nil {
os.Exit(1)
return
}
os.Exit(0)
}
}
func commaSeparatedToSlice(s []string) []string {
var result []string
for _, items := range s {
for _, item := range strings.Split(items, ",") {
item = strings.TrimSpace(item)
if item == "" {
continue
}
result = append(result, item)
}
}
return result
}
func printAverageDetectorTime(e *engine.Engine) {
fmt.Fprintln(
os.Stderr,
"Average detector time is the measurement of average time spent on each detector when results are returned.",
)
for detectorName, duration := range e.GetDetectorsMetrics() {
fmt.Fprintf(os.Stderr, "%s: %s\n", detectorName, duration)
}
}
// validateClonePath ensures that --clone-path, if provided, exists and is a directory.
// It also verifies that --no-cleanup is only allowed when --clone-path is set.
// Note: without a custom clone path, repositories are cloned into temporary directories, which should never be retained.
func validateClonePath(clonePath string, noCleanup bool) error {
if noCleanup && clonePath == "" {
return fmt.Errorf("invalid configuration: --no-cleanup can only be used together with --clone-path")
}
if clonePath == "" {
return nil
}
info, err := os.Stat(clonePath)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("path provided to --clone-path: %q does not exist", clonePath)
}
return fmt.Errorf("failed to access --clone-path %q: %w", clonePath, err)
}
if !info.IsDir() {
return fmt.Errorf("path provided to --clone-path: %q is not a directory", clonePath)
}
return nil
}
// isPreCommitHook detects if trufflehog is running as a pre-commit hook
func isPreCommitHook() bool {
// Pre-commit.com framework detection
// Docs: https://pre-commit.com/#pre-commit
// Sets PRE_COMMIT=1 environment variable when running hooks
if os.Getenv("PRE_COMMIT") == "1" {
return true
}
// Husky framework detection (modern versions)
// Docs: https://typicode.github.io/husky/get-started.html#disabling-hooks
// Sets HUSKY=1 environment variable for all hooks
if os.Getenv("HUSKY") == "1" {
return true
}
// Husky legacy detection (versions < 4.0)
// Sets HUSKY_GIT_PARAMS for git hooks, containing commit parameters
// Reference: https://github.com/typicode/husky/tree/v0.14.3
if os.Getenv("HUSKY_GIT_PARAMS") != "" {
return true
}
// Local Git hook detection (non-framework)
// Native Git hooks don't set specific environment variables by default.
// To detect local hooks without frameworks, we must explicitly set
// an environment variable in the hook script:
// Example in .git/hooks/pre-commit:
// export TRUFFLEHOG_PRE_COMMIT=1
// Than we can detect it
if os.Getenv("TRUFFLEHOG_PRE_COMMIT") == "1" {
return true
}
return false
}
================================================
FILE: pkg/analyzer/README.md
================================================
# Implementing Analyzers
## Defining the Permissions
Permissions can be defined in:
- lower snake case as `permission_name:access_level`
- kebab case as `permission-name:read`
- dot notation as `permission.name:read`
The Permissions are initially defined as a [yaml file](analyzers/twilio/permissions.yaml).
At the top of the [analyzer implementation](analyzers/twilio/twilio.go) you specify the go generate command.
You can install the generator with `go install github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/generate_permissions`.
Then you can run `go generate ./...` to generate the Permission types for the analyzer.
The generated Permission types are to be used in the `AnalyzerResult` struct when defining the `Permissions` and in your code.
================================================
FILE: pkg/analyzer/analyzers/airbrake/airbrake.go
================================================
package airbrake
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeAirbrake }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
info, err := AnalyzePermissions(a.Cfg, credInfo["key"])
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
Metadata: map[string]any{
"key_type": info.KeyType,
"reference": info.Reference,
},
}
// Copy the rest of the metadata over.
for k, v := range info.Misc {
result.Metadata[k] = v
}
// Build a list of Bindings by referencing the same permissions list
// for each resource.
permissions := allPermissions()
for _, proj := range info.Projects {
resource := analyzers.Resource{
Name: proj.Name,
FullyQualifiedName: strconv.Itoa(proj.ID),
Type: "project",
}
for _, perm := range permissions {
binding := analyzers.Binding{
Resource: resource,
Permission: perm,
}
result.Bindings = append(result.Bindings, binding)
}
}
return &result
}
type SecretInfo struct {
KeyType string
Projects []Project
Reference string
Scopes []analyzers.Permission
Misc map[string]string
}
type Project struct {
Name string `json:"name"`
ID int `json:"id"`
}
// validateKey checks if the key is valid and returns the projects associated with the key
func validateKey(cfg *config.Config, key string) (bool, []Project, error) {
type ProjectsJSON struct {
Projects []Project `json:"projects"`
}
// create struct to hold response
var projects ProjectsJSON
// create http client
client := analyzers.NewAnalyzeClient(cfg)
// create request
req, err := http.NewRequest("GET", "https://api.airbrake.io/api/v4/projects", nil)
if err != nil {
return false, projects.Projects, err
}
// add key as url param
q := req.URL.Query()
q.Add("key", key)
req.URL.RawQuery = q.Encode()
// send request
resp, err := client.Do(req)
if err != nil {
return false, projects.Projects, err
}
// read response
defer resp.Body.Close()
// if status code is 200, decode response
if resp.StatusCode == 200 {
err := json.NewDecoder(resp.Body).Decode(&projects)
return true, projects.Projects, err
}
// if status code is not 200, return false
return false, projects.Projects, nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] %s", err.Error())
return
}
color.Green("[!] Valid Airbrake User API Key\n\n")
color.Green("[i] Key Type: " + info.KeyType)
if v, ok := info.Misc["expiration"]; ok {
color.Green("[i] Expiration: %s", v)
}
if v, ok := info.Misc["duration"]; ok {
color.Green("[i] Duration: %s", v)
}
color.Green("\n[i] Projects:")
printProjects(info.Projects...)
color.Green("\n[i] Permissions:")
printPermissions(info.Scopes)
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
valid, projects, err := validateKey(cfg, key)
if err != nil {
return nil, err
}
if !valid {
return nil, fmt.Errorf("Invalid Airbrake User API Key")
}
info := &SecretInfo{
Projects: projects,
Reference: "https://docs.airbrake.io/docs/devops-tools/api/",
// If the token exists, it has all permissions.
Scopes: allPermissions(),
Misc: make(map[string]string),
}
if len(key) == 40 {
info.KeyType = "User Key"
info.Misc["expiration"] = "Never"
} else {
info.KeyType = "User Token"
info.Misc["duration"] = "Short Lived"
}
return info, nil
}
func allPermissions() []analyzers.Permission {
permissions := make([]analyzers.Permission, len(scope_order))
for i, perm := range scope_order {
permissions[i] = analyzers.Permission{Value: perm}
}
return permissions
}
func printProjects(projects ...Project) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Project ID", "Project Name"})
for _, project := range projects {
t.AppendRow([]any{color.GreenString("%d", project.ID), color.GreenString("%s", project.Name)})
}
t.Render()
}
func printPermissions(scopes []analyzers.Permission) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "Permissions"})
for _, scope := range scopes {
scope := scope.Value
for i, permission := range scope_mapping[scope] {
if i == 0 {
t.AppendRow([]any{color.GreenString("%s", scope), color.GreenString("%s", permission)})
continue
}
}
}
t.Render()
fmt.Println("| Ref: https://docs.airbrake.io/docs/devops-tools/api/ |")
fmt.Println("+------------------------+---------------------------------+")
}
================================================
FILE: pkg/analyzer/analyzers/airbrake/scopes.go
================================================
package airbrake
var scope_order = []string{
"Authentication",
"Performance Monitoring",
"Error Notification",
"Projects",
"Deploys",
"Groups",
"Notices",
"Project Activities",
"Source Maps",
"iOS Crash Reports",
}
var scope_mapping = map[string][]string{
"Authentication": {"Create user token"},
"Performance Monitoring": {"Route performance endpoint", "Routes breakdown endpoint", "Database query stats", "Queue stats"},
"Error Notification": {"Create notice"},
"Projects": {"List projects", "Show projects"},
"Deploys": {"Create deploy", "List deploys", "Show deploy"},
"Groups": {"List groups", "Show group", "Mute group", "Unmute group", "Delete group", "List groups across all projects", "Show group statistics"},
"Notices": {"List notices", "Show notice status"},
"Project Activities": {"List project activities", "Show project statistics"},
"Source Maps": {"Create source map", "List source maps", "Show source map", "Delete source map"},
"iOS Crash Reports": {"Create iOS crash report"},
}
================================================
FILE: pkg/analyzer/analyzers/airtable/airtableoauth/airtable.go
================================================
package airtableoauth
import (
"errors"
"github.com/fatih/color"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/airtable/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeAirtableOAuth }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
token, ok := credInfo["token"]
if !ok {
return nil, errors.New("token not found in credInfo")
}
userInfo, err := common.FetchAirtableUserInfo(token)
if err != nil {
return nil, err
}
var basesInfo *common.AirtableBases
baseScope := common.PermissionStrings[common.SchemaBasesRead]
if hasScope(userInfo.Scopes, baseScope) {
basesInfo, _ = common.FetchAirtableBases(token)
}
return common.MapToAnalyzerResult(userInfo, basesInfo), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, token string) {
userInfo, err := common.FetchAirtableUserInfo(token)
if err != nil {
color.Red("[x] Error : %s", err.Error())
return
}
color.Green("[!] Valid Airtable OAuth2 Access Token\n\n")
printUserAndPermissions(userInfo)
baseScope := common.PermissionStrings[common.SchemaBasesRead]
if hasScope(userInfo.Scopes, baseScope) {
var basesInfo *common.AirtableBases
basesInfo, _ = common.FetchAirtableBases(token)
common.PrintBases(basesInfo)
}
}
func hasScope(scopes []string, target string) bool {
for _, scope := range scopes {
if scope == target {
return true
}
}
return false
}
func printUserAndPermissions(info *common.AirtableUserInfo) {
scopeStatusMap := make(map[string]bool)
for _, scope := range common.PermissionStrings {
scopeStatusMap[scope] = false
}
for _, scope := range info.Scopes {
scopeStatusMap[scope] = true
}
common.PrintUserAndPermissions(info, scopeStatusMap)
}
================================================
FILE: pkg/analyzer/analyzers/airtable/airtableoauth/airtable_test.go
================================================
package airtableoauth
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(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)
}
tests := []struct {
name string
token string
want string // JSON string
wantErr bool
}{
{
token: testSecrets.MustGetField("AIRTABLEOAUTH_TOKEN"),
name: "valid Airtable OAuth Token",
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"token": tt.token})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/airtable/airtableoauth/expected_output.json
================================================
{
"AnalyzerType": 28,
"Bindings": [
{
"Resource": {
"Name": "usraS0CjAASH3XMpU",
"FullyQualifiedName": "usraS0CjAASH3XMpU",
"Type": "user",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "data.records:read",
"Parent": null
}
},
{
"Resource": {
"Name": "usraS0CjAASH3XMpU",
"FullyQualifiedName": "usraS0CjAASH3XMpU",
"Type": "user",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "schema.bases:read",
"Parent": null
}
}
],
"UnboundedResources": [
{
"Name": "Client Leads and Sales Management",
"FullyQualifiedName": "appzRyj5Q9R9kK6cF",
"Type": "base",
"Parent": null
}
]
}
================================================
FILE: pkg/analyzer/analyzers/airtable/airtablepat/airtable.go
================================================
package airtablepat
import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/fatih/color"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/airtable/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeAirtablePat }
var scopeStatusMap = make(map[string]bool)
func getEndpoint(endpointName common.EndpointName) (common.Endpoint, bool) {
return common.GetEndpoint(endpointName)
}
func getScopeEndpoint(scope string) (common.Endpoint, bool) {
return common.GetScopeEndpoint(scope)
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
token, ok := credInfo["token"]
if !ok {
return nil, errors.New("token not found in credInfo")
}
userInfo, err := common.FetchAirtableUserInfo(token)
if err != nil {
return nil, err
}
scopeStatusMap[common.PermissionStrings[common.UserEmailRead]] = userInfo.Email != nil
var basesInfo *common.AirtableBases
granted, err := determineScope(token, common.SchemaBasesRead, nil)
if err != nil {
return nil, err
}
if granted {
basesInfo, err = common.FetchAirtableBases(token)
if err != nil {
return nil, err
}
// If bases are fetched, determine the token scopes
err := determineScopes(token, basesInfo)
if err != nil {
return nil, err
}
}
return mapToAnalyzerResult(userInfo, basesInfo), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, token string) {
userInfo, err := common.FetchAirtableUserInfo(token)
if err != nil {
color.Red("[x] Error : %s", err.Error())
return
}
scopeStatusMap[common.PermissionStrings[common.UserEmailRead]] = userInfo.Email != nil
var basesInfo *common.AirtableBases
basesReadPermission := common.SchemaBasesRead
if granted, err := determineScope(token, basesReadPermission, nil); granted {
if err != nil {
color.Red("[x] Error : %s", err.Error())
return
}
basesInfo, _ = common.FetchAirtableBases(token)
err := determineScopes(token, basesInfo)
if err != nil {
color.Red("[x] Error : %s", err.Error())
return
}
}
color.Green("[!] Valid Airtable Personal Access Token\n\n")
common.PrintUserAndPermissions(userInfo, scopeStatusMap)
if scopeStatusMap[common.PermissionStrings[basesReadPermission]] {
common.PrintBases(basesInfo)
}
}
// determineScope checks whether the given token has the specified permission by making an API call.
//
// The function performs the following actions:
// - Determines the appropriate API Endpoint based on the input scope/permission.
// - Constructs an HTTP request using the endpoint's URL, method, and required IDs.
// If the URL contains path parameters (e.g., "{baseID}"), they must be replaced using `requiredIDs`.
// - Sends the request and analyzes the response to determine if the token has the requested permission.
//
// Returns `true` if the token has the permission, `false` otherwise.
// If an error occurs, it returns false along with the encountered error.
func determineScope(token string, perm common.Permission, requiredIDs map[string]string) (bool, error) {
scopeString := common.PermissionStrings[perm]
endpoint, exists := getScopeEndpoint(scopeString)
if !exists {
return false, nil
}
url := endpoint.URL
if requiredIDs != nil {
for _, key := range endpoint.RequiredIDs {
if value, ok := requiredIDs[key]; ok {
url = strings.Replace(url, fmt.Sprintf("{%s}", key), value, -1)
}
}
}
resp, err := common.CallAirtableAPI(token, endpoint.Method, url)
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode == endpoint.ExpectedSuccessStatus {
scopeStatusMap[scopeString] = true
return true, nil
}
// If the response status is not 200 OK, we need to verify if the error is as expected
if endpoint.ExpectedErrorResponse != nil {
var result map[string]any
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return false, err
}
errorInfo, ok := result["error"].(map[string]any)
if !ok {
// If no error is found in the response, the scope is unverified
return false, nil
}
errorType, ok := errorInfo["type"].(string)
if !ok || errorType != endpoint.ExpectedErrorResponse.Type {
// If "type" is missing from the error body, or mismatches the expected type, the scope is unverified
return false, nil
}
// The token lacks the scope/permission to fulfill the request
scopeStatusMap[scopeString] = false
return false, nil
}
// Can not determine scope as the expected error is unknown
return false, nil
}
func determineScopes(token string, basesInfo *common.AirtableBases) error {
if basesInfo == nil || len(basesInfo.Bases) == 0 {
return nil
}
for _, base := range basesInfo.Bases {
requiredIDs := map[string]string{"baseID": base.ID}
tableScopesDetermined := false
// Verify token "webhooks:manage" permission
_, err := determineScope(token, common.WebhookManage, requiredIDs)
if err != nil {
return err
}
// Verify token "block:manage" permission
_, err = determineScope(token, common.BlockManage, requiredIDs)
if err != nil {
return err
}
if base.Schema == nil || len(base.Schema.Tables) == 0 {
return nil
}
// Verifying scopes that require an existing table
for _, table := range base.Schema.Tables {
requiredIDs["tableID"] = table.ID
if !tableScopesDetermined {
_, err = determineScope(token, common.SchemaBasesWrite, requiredIDs)
if err != nil {
return err
}
_, err = determineScope(token, common.DataRecordsWrite, requiredIDs)
if err != nil {
return err
}
tableScopesDetermined = true
}
granted, err := determineScope(token, common.DataRecordsRead, requiredIDs)
if err != nil {
return err
}
if !granted {
continue
}
// Verifying scopes that require an existing "record" and the "data records read" permission
records, err := fetchAirtableRecords(token, base.ID, table.ID)
if err != nil {
return err
}
for _, record := range records {
requiredIDs["recordID"] = record.ID
_, err = determineScope(token, common.DataRecordcommentsRead, requiredIDs)
if err != nil {
return err
}
break
}
if len(records) != 0 {
break
}
}
}
return nil
}
func mapToAnalyzerResult(userInfo *common.AirtableUserInfo, basesInfo *common.AirtableBases) *analyzers.AnalyzerResult {
for scope, status := range scopeStatusMap {
if status {
userInfo.Scopes = append(userInfo.Scopes, scope)
}
}
return common.MapToAnalyzerResult(userInfo, basesInfo)
}
================================================
FILE: pkg/analyzer/analyzers/airtable/airtablepat/airtable_test.go
================================================
package airtablepat
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(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)
}
tests := []struct {
name string
token string
want string // JSON string
wantErr bool
}{
{
token: testSecrets.MustGetField("AIRTABLEOAUTH_TOKEN"),
name: "valid Airtable Personal Access Token",
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"token": tt.token})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/airtable/airtablepat/expected_output.json
================================================
{
"AnalyzerType": 29,
"Bindings": [
{
"Resource": {
"Name": "usraS0CjAASH3XMpU",
"FullyQualifiedName": "usraS0CjAASH3XMpU",
"Type": "user",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "data.records:read",
"Parent": null
}
},
{
"Resource": {
"Name": "usraS0CjAASH3XMpU",
"FullyQualifiedName": "usraS0CjAASH3XMpU",
"Type": "user",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "schema.bases:read",
"Parent": null
}
}
],
"UnboundedResources": [
{
"Name": "Client Leads and Sales Management",
"FullyQualifiedName": "appzRyj5Q9R9kK6cF",
"Type": "base",
"Parent": null
}
]
}
================================================
FILE: pkg/analyzer/analyzers/airtable/airtablepat/requests.go
================================================
package airtablepat
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/airtable/common"
)
type AirtableRecordsResponse struct {
Records []common.AirtableEntity `json:"records"`
}
func fetchAirtableRecords(token string, baseID string, tableID string) ([]common.AirtableEntity, error) {
endpoint, exists := getEndpoint(common.ListRecordsEndpoint)
if !exists {
return nil, fmt.Errorf("endpoint for ListRecordsEndpoint does not exist")
}
url := strings.Replace(strings.Replace(endpoint.URL, "{baseID}", baseID, -1), "{tableID}", tableID, -1)
resp, err := common.CallAirtableAPI(token, "GET", url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch Airtable records, status: %d", resp.StatusCode)
}
var recordsResponse AirtableRecordsResponse
if err := json.NewDecoder(resp.Body).Decode(&recordsResponse); err != nil {
return nil, err
}
return recordsResponse.Records, nil
}
================================================
FILE: pkg/analyzer/analyzers/airtable/common/common.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go common
package common
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
)
func CallAirtableAPI(token string, method string, url string) (*http.Response, error) {
req, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
resp, err := detectors.DetectorHttpClientWithNoLocalAddresses.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}
func FetchAirtableUserInfo(token string) (*AirtableUserInfo, error) {
endpoint, exists := GetEndpoint(GetUserInfoEndpoint)
if !exists {
return nil, fmt.Errorf("endpoint for GetUserInfoEndpoint does not exist")
}
resp, err := CallAirtableAPI(token, endpoint.Method, endpoint.URL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch Airtable user info, status: %d", resp.StatusCode)
}
var userInfo AirtableUserInfo
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
return nil, err
}
return &userInfo, nil
}
func FetchAirtableBases(token string) (*AirtableBases, error) {
endpoint, exists := GetEndpoint(ListBasesEndpoint)
if !exists {
return nil, fmt.Errorf("endpoint for ListBasesEndpoint does not exist")
}
resp, err := CallAirtableAPI(token, endpoint.Method, endpoint.URL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch Airtable bases, status: %d", resp.StatusCode)
}
var basesInfo AirtableBases
if err := json.NewDecoder(resp.Body).Decode(&basesInfo); err != nil {
return nil, err
}
// Fetch schema for each base
for i, base := range basesInfo.Bases {
schema, err := fetchBaseSchema(token, base.ID)
if err != nil {
basesInfo.Bases[i].Schema = nil
} else {
basesInfo.Bases[i].Schema = schema
}
}
return &basesInfo, nil
}
func fetchBaseSchema(token string, baseID string) (*Schema, error) {
endpoint, exists := GetEndpoint(GetBaseSchemaEndpoint)
if !exists {
return nil, fmt.Errorf("endpoint for GetBaseSchemaEndpoint does not exist")
}
url := strings.ReplaceAll(endpoint.URL, "{baseID}", baseID)
resp, err := CallAirtableAPI(token, endpoint.Method, url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch schema for base %s, status: %d", baseID, resp.StatusCode)
}
var schema Schema
if err := json.NewDecoder(resp.Body).Decode(&schema); err != nil {
return nil, err
}
return &schema, nil
}
func MapToAnalyzerResult(userInfo *AirtableUserInfo, basesInfo *AirtableBases) *analyzers.AnalyzerResult {
if userInfo == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeAirtableOAuth,
}
var permissions []analyzers.Permission
for _, scope := range userInfo.Scopes {
permissions = append(permissions, analyzers.Permission{Value: scope})
}
userResource := analyzers.Resource{
Name: userInfo.ID,
FullyQualifiedName: userInfo.ID,
Type: "user",
Metadata: map[string]any{},
}
if userInfo.Email != nil {
userResource.Metadata["email"] = *userInfo.Email
}
result.Bindings = analyzers.BindAllPermissions(userResource, permissions...)
if basesInfo != nil {
for _, base := range basesInfo.Bases {
resource := analyzers.Resource{
Name: base.Name,
FullyQualifiedName: base.ID,
Type: "base",
}
result.UnboundedResources = append(result.UnboundedResources, resource)
}
}
return &result
}
func PrintUserAndPermissions(info *AirtableUserInfo, scopeStatusMap map[string]bool) {
color.Yellow("[i] User:")
t1 := table.NewWriter()
email := "N/A"
if info.Email != nil {
email = *info.Email
}
t1.SetOutputMirror(os.Stdout)
t1.AppendHeader(table.Row{"ID", "Email"})
t1.AppendRow(table.Row{color.GreenString(info.ID), color.GreenString(email)})
t1.SetOutputMirror(os.Stdout)
t1.Render()
color.Yellow("\n[i] Scopes:")
t2 := table.NewWriter()
t2.SetOutputMirror(os.Stdout)
t2.AppendHeader(table.Row{"Scope", "Permission", "Status"})
for _, scope := range PermissionStrings {
scopeStatus := "Could not verify"
if status, ok := scopeStatusMap[scope]; ok {
if status {
scopeStatus = "Granted"
} else {
scopeStatus = "Denied"
}
}
permissions, ok := GetScopePermissions(scope)
if !ok {
continue
}
for i, permission := range permissions {
scopeString := ""
if i == 0 {
scopeString = scope
}
t2.AppendRow(table.Row{color.GreenString(scopeString), color.GreenString(permission), color.GreenString(scopeStatus)})
scopeStatus = ""
}
t2.AppendSeparator()
}
t2.Render()
fmt.Printf("%s: https://airtable.com/developers/web/api/scopes\n", color.GreenString("Ref"))
}
func PrintBases(bases *AirtableBases) {
color.Yellow("\n[i] Bases:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
if len(bases.Bases) > 0 {
t.AppendHeader(table.Row{"ID", "Name"})
for _, base := range bases.Bases {
t.AppendRow(table.Row{color.GreenString(base.ID), color.GreenString(base.Name)})
}
} else {
fmt.Printf("%s\n", color.GreenString("No bases associated with token"))
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/airtable/common/endpoints.go
================================================
package common
import "net/http"
type ErrorResponse struct {
Type string
}
type Endpoint struct {
URL string
Method string
RequiredIDs []string
RequiredPermission *string
ExpectedSuccessStatus int
ExpectedErrorResponse *ErrorResponse
}
type EndpointName int
const (
GetUserInfoEndpoint EndpointName = iota
ListBasesEndpoint EndpointName = iota
UpdateBaseEndpoint EndpointName = iota
GetBaseSchemaEndpoint EndpointName = iota
ListRecordsEndpoint EndpointName = iota
CreateRecordEndpoint EndpointName = iota
ListRecordCommentsEndpoint EndpointName = iota
ListWebhooksEndpoint EndpointName = iota
ListBlockInstallationsEndpoint EndpointName = iota
)
var endpoints map[EndpointName]Endpoint
func init() {
endpoints = map[EndpointName]Endpoint{
GetUserInfoEndpoint: {
URL: "https://api.airtable.com/v0/meta/whoami",
Method: "GET",
},
ListBasesEndpoint: {
URL: "https://api.airtable.com/v0/meta/bases",
Method: "GET",
RequiredPermission: GetRequiredPermission(SchemaBasesRead),
ExpectedSuccessStatus: http.StatusOK,
ExpectedErrorResponse: &ErrorResponse{
Type: "INVALID_PERMISSIONS_OR_MODEL_NOT_FOUND",
},
},
UpdateBaseEndpoint: {
URL: "https://api.airtable.com/v0/meta/bases/{baseID}/tables/{tableID}",
Method: "PATCH",
RequiredIDs: []string{"baseID", "tableID"},
RequiredPermission: GetRequiredPermission(SchemaBasesWrite),
ExpectedSuccessStatus: http.StatusUnprocessableEntity,
ExpectedErrorResponse: &ErrorResponse{
Type: "INVALID_PERMISSIONS_OR_MODEL_NOT_FOUND",
},
},
GetBaseSchemaEndpoint: {
URL: "https://api.airtable.com/v0/meta/bases/{baseID}/tables",
Method: "GET",
RequiredIDs: []string{"baseID"},
RequiredPermission: GetRequiredPermission(SchemaBasesRead),
ExpectedSuccessStatus: http.StatusOK,
ExpectedErrorResponse: &ErrorResponse{
Type: "INVALID_PERMISSIONS_OR_MODEL_NOT_FOUND",
},
},
ListRecordsEndpoint: {
URL: "https://api.airtable.com/v0/{baseID}/{tableID}",
Method: "GET",
RequiredIDs: []string{"baseID", "tableID"},
RequiredPermission: GetRequiredPermission(DataRecordsRead),
ExpectedSuccessStatus: http.StatusOK,
ExpectedErrorResponse: &ErrorResponse{
Type: "INVALID_PERMISSIONS_OR_MODEL_NOT_FOUND",
},
},
CreateRecordEndpoint: {
URL: "https://api.airtable.com/v0/{baseID}/{tableID}",
Method: "POST",
RequiredIDs: []string{"baseID", "tableID"},
RequiredPermission: GetRequiredPermission(DataRecordsWrite),
ExpectedSuccessStatus: http.StatusUnprocessableEntity,
ExpectedErrorResponse: &ErrorResponse{
Type: "INVALID_PERMISSIONS_OR_MODEL_NOT_FOUND",
},
},
ListRecordCommentsEndpoint: {
URL: "https://api.airtable.com/v0/{baseID}/{tableID}/{recordID}/comments",
Method: "GET",
RequiredIDs: []string{"baseID", "tableID", "recordID"},
RequiredPermission: GetRequiredPermission(DataRecordcommentsRead),
ExpectedSuccessStatus: http.StatusOK,
ExpectedErrorResponse: &ErrorResponse{
Type: "INVALID_PERMISSIONS_OR_MODEL_NOT_FOUND",
},
},
ListWebhooksEndpoint: {
URL: "https://api.airtable.com/v0/bases/{baseID}/webhooks",
Method: "GET",
RequiredIDs: []string{"baseID"},
RequiredPermission: GetRequiredPermission(WebhookManage),
ExpectedSuccessStatus: http.StatusOK,
ExpectedErrorResponse: &ErrorResponse{
Type: "INVALID_PERMISSIONS_OR_MODEL_NOT_FOUND",
},
},
ListBlockInstallationsEndpoint: {
URL: "https://api.airtable.com/v0/meta/bases/{baseID}/blockInstallations",
Method: "GET",
RequiredIDs: []string{"baseID"},
RequiredPermission: GetRequiredPermission(BlockManage),
ExpectedSuccessStatus: http.StatusOK,
ExpectedErrorResponse: &ErrorResponse{
Type: "INVALID_PERMISSIONS_OR_MODEL_NOT_FOUND",
},
},
}
}
func GetRequiredPermission(permission Permission) *string {
if val, exists := PermissionStrings[permission]; exists {
return &val
}
return nil
}
// GetEndpoint returns the endpoint object for the provided name and whether it exists
func GetEndpoint(name EndpointName) (Endpoint, bool) {
endpoint, exists := endpoints[name]
return endpoint, exists
}
================================================
FILE: pkg/analyzer/analyzers/airtable/common/models.go
================================================
package common
type AirtableUserInfo struct {
ID string `json:"id"`
Email *string `json:"email,omitempty"`
Scopes []string `json:"scopes"`
}
type AirtableBases struct {
Bases []struct {
ID string `json:"id"`
Name string `json:"name"`
Schema *Schema `json:"schema,omitempty"`
} `json:"bases"`
}
type Schema struct {
Tables []AirtableEntity `json:"tables"`
}
type AirtableEntity struct {
ID string `json:"id"`
}
================================================
FILE: pkg/analyzer/analyzers/airtable/common/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package common
import "errors"
type Permission int
const (
Invalid Permission = iota
DataRecordsRead Permission = iota
DataRecordsWrite Permission = iota
DataRecordcommentsRead Permission = iota
DataRecordcommentsWrite Permission = iota
SchemaBasesRead Permission = iota
SchemaBasesWrite Permission = iota
WebhookManage Permission = iota
BlockManage Permission = iota
UserEmailRead Permission = iota
EnterpriseGroupsRead Permission = iota
WorkspacesandbasesRead Permission = iota
WorkspacesandbasesWrite Permission = iota
WorkspacesandbasesSharesManage Permission = iota
EnterpriseScimUsersandgroupsManage Permission = iota
EnterpriseAuditlogsRead Permission = iota
EnterpriseChangeeventsRead Permission = iota
EnterpriseExportsManage Permission = iota
EnterpriseAccountRead Permission = iota
EnterpriseAccountWrite Permission = iota
EnterpriseUserRead Permission = iota
EnterpriseUserWrite Permission = iota
EnterpriseGroupsManage Permission = iota
WorkspacesandbasesManage Permission = iota
)
var (
PermissionStrings = map[Permission]string{
DataRecordsRead: "data.records:read",
DataRecordsWrite: "data.records:write",
DataRecordcommentsRead: "data.recordComments:read",
DataRecordcommentsWrite: "data.recordComments:write",
SchemaBasesRead: "schema.bases:read",
SchemaBasesWrite: "schema.bases:write",
WebhookManage: "webhook:manage",
BlockManage: "block:manage",
UserEmailRead: "user.email:read",
EnterpriseGroupsRead: "enterprise.groups:read",
WorkspacesandbasesRead: "workspacesAndBases:read",
WorkspacesandbasesWrite: "workspacesAndBases:write",
WorkspacesandbasesSharesManage: "workspacesAndBases.shares:manage",
EnterpriseScimUsersandgroupsManage: "enterprise.scim.usersAndGroups:manage",
EnterpriseAuditlogsRead: "enterprise.auditLogs:read",
EnterpriseChangeeventsRead: "enterprise.changeEvents:read",
EnterpriseExportsManage: "enterprise.exports:manage",
EnterpriseAccountRead: "enterprise.account:read",
EnterpriseAccountWrite: "enterprise.account:write",
EnterpriseUserRead: "enterprise.user:read",
EnterpriseUserWrite: "enterprise.user:write",
EnterpriseGroupsManage: "enterprise.groups:manage",
WorkspacesandbasesManage: "workspacesAndBases:manage",
}
StringToPermission = map[string]Permission{
"data.records:read": DataRecordsRead,
"data.records:write": DataRecordsWrite,
"data.recordComments:read": DataRecordcommentsRead,
"data.recordComments:write": DataRecordcommentsWrite,
"schema.bases:read": SchemaBasesRead,
"schema.bases:write": SchemaBasesWrite,
"webhook:manage": WebhookManage,
"block:manage": BlockManage,
"user.email:read": UserEmailRead,
"enterprise.groups:read": EnterpriseGroupsRead,
"workspacesAndBases:read": WorkspacesandbasesRead,
"workspacesAndBases:write": WorkspacesandbasesWrite,
"workspacesAndBases.shares:manage": WorkspacesandbasesSharesManage,
"enterprise.scim.usersAndGroups:manage": EnterpriseScimUsersandgroupsManage,
"enterprise.auditLogs:read": EnterpriseAuditlogsRead,
"enterprise.changeEvents:read": EnterpriseChangeeventsRead,
"enterprise.exports:manage": EnterpriseExportsManage,
"enterprise.account:read": EnterpriseAccountRead,
"enterprise.account:write": EnterpriseAccountWrite,
"enterprise.user:read": EnterpriseUserRead,
"enterprise.user:write": EnterpriseUserWrite,
"enterprise.groups:manage": EnterpriseGroupsManage,
"workspacesAndBases:manage": WorkspacesandbasesManage,
}
PermissionIDs = map[Permission]int{
DataRecordsRead: 1,
DataRecordsWrite: 2,
DataRecordcommentsRead: 3,
DataRecordcommentsWrite: 4,
SchemaBasesRead: 5,
SchemaBasesWrite: 6,
WebhookManage: 7,
BlockManage: 8,
UserEmailRead: 9,
EnterpriseGroupsRead: 10,
WorkspacesandbasesRead: 11,
WorkspacesandbasesWrite: 12,
WorkspacesandbasesSharesManage: 13,
EnterpriseScimUsersandgroupsManage: 14,
EnterpriseAuditlogsRead: 15,
EnterpriseChangeeventsRead: 16,
EnterpriseExportsManage: 17,
EnterpriseAccountRead: 18,
EnterpriseAccountWrite: 19,
EnterpriseUserRead: 20,
EnterpriseUserWrite: 21,
EnterpriseGroupsManage: 22,
WorkspacesandbasesManage: 23,
}
IdToPermission = map[int]Permission{
1: DataRecordsRead,
2: DataRecordsWrite,
3: DataRecordcommentsRead,
4: DataRecordcommentsWrite,
5: SchemaBasesRead,
6: SchemaBasesWrite,
7: WebhookManage,
8: BlockManage,
9: UserEmailRead,
10: EnterpriseGroupsRead,
11: WorkspacesandbasesRead,
12: WorkspacesandbasesWrite,
13: WorkspacesandbasesSharesManage,
14: EnterpriseScimUsersandgroupsManage,
15: EnterpriseAuditlogsRead,
16: EnterpriseChangeeventsRead,
17: EnterpriseExportsManage,
18: EnterpriseAccountRead,
19: EnterpriseAccountWrite,
20: EnterpriseUserRead,
21: EnterpriseUserWrite,
22: EnterpriseGroupsManage,
23: WorkspacesandbasesManage,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/airtable/common/permissions.yaml
================================================
permissions:
- data.records:read
- data.records:write
- data.recordComments:read
- data.recordComments:write
- schema.bases:read
- schema.bases:write
- webhook:manage
- block:manage
- user.email:read
- enterprise.groups:read
- workspacesAndBases:read
- workspacesAndBases:write
- workspacesAndBases.shares:manage
- enterprise.scim.usersAndGroups:manage
- enterprise.auditLogs:read
- enterprise.changeEvents:read
- enterprise.exports:manage
- enterprise.account:read
- enterprise.account:write
- enterprise.user:read
- enterprise.user:write
- enterprise.groups:manage
- workspacesAndBases:manage
================================================
FILE: pkg/analyzer/analyzers/airtable/common/scopes.go
================================================
package common
var scopeToPermissions = map[string][]string{
// Basic Scopes
"data.records:read": {
"List records",
"Get record",
},
"data.records:write": {
"Create records",
"Update record",
"Update multiple records",
"Delete record",
"Delete multiple records",
"Sync CSV data",
},
"data.recordComments:read": {
"List comments",
},
"data.recordComments:write": {
"Create comment",
"Delete comment",
"Update comment",
},
"schema.bases:read": {
"List bases",
"Get base schema",
},
"schema.bases:write": {
"Create base",
"Create table",
"Update table",
"Create field",
"Update field",
"Sync CSV data",
},
"webhook:manage": {
"List webhooks",
"Create a webhook",
"Delete a webhook",
"Enable/disable webhook notifications",
"Refresh a webhook",
},
"block:manage": {
"Create new releases and submissions for custom extensions",
},
"user.email:read": {
"See the user's email address",
},
// Enterprise scopes
"enterprise.groups:read": {
"Get user group",
},
"workspacesAndBases:read": {
"Get base collaborators",
"List block installations",
"Get interface",
"List views",
"Get view metadata",
"Get workspace collaborators",
},
"workspacesAndBases:write": {
"Delete block installation",
"Manage block installation",
"Add base collaborator",
"Delete base collaborator",
"Update collaborator base permission",
"Add interface collaborator",
"Delete interface collaborator",
"Update interface collaborator",
"Delete interface invite",
"Delete base invite",
"Delete view",
"Add workspace collaborator",
"Delete workspace collaborator",
"Update workspace collaborator",
"Delete workspace invite",
"Update workspace restrictions",
},
"workspacesAndBases.shares:manage": {
"List shares",
"Delete share",
"Manage share",
},
"enterprise.scim.usersAndGroups:manage": {
"List groups",
"Create group",
"Delete group",
"Get group",
"Patch group",
"Put group",
"List users",
"Create user",
"Delete user",
"Get user",
"Patch user",
"Put user",
},
"enterprise.auditLogs:read": {
"Audit log events",
"List audit log requests",
"Create audit log request",
"Get audit log request",
},
"enterprise.changeEvents:read": {
"Change events",
},
"enterprise.exports:manage": {
"List eDiscovery exports",
"Create eDiscovery export",
"Get eDiscovery export",
},
"enterprise.account:read": {
"Get enterprise",
},
"enterprise.account:write": {
"Create descendant enterprise",
},
"enterprise.user:read": {
"Get users by id or email",
"Get user by id",
},
"enterprise.user:write": {
"Delete users by email",
"Manage user batched",
"Manage user membership",
"Grant admin access",
"Revoke admin access",
"Delete user by id",
"Manage user",
"Logout user",
"Remove user from enterprise",
},
"enterprise.groups:manage": {
"Move user groups",
},
"workspacesAndBases:manage": {
"Delete base",
"Move workspaces",
"Delete workspace",
"Move base",
},
}
var scopeToEndpointName = map[string]EndpointName{
"schema.bases:read": ListBasesEndpoint,
"schema.bases:write": UpdateBaseEndpoint,
"webhook:manage": ListWebhooksEndpoint,
"block:manage": ListBlockInstallationsEndpoint,
"data.records:read": ListRecordsEndpoint,
"data.records:write": CreateRecordEndpoint,
"data.recordComments:read": ListRecordCommentsEndpoint,
}
var scopeToEndpoint map[string]Endpoint
func init() {
scopeToEndpoint = make(map[string]Endpoint)
for scope, endpointName := range scopeToEndpointName {
if endpoint, exists := GetEndpoint(endpointName); exists {
scopeToEndpoint[scope] = endpoint
}
}
}
func GetScopePermissions(scope string) ([]string, bool) {
permission, exists := scopeToPermissions[scope]
return permission, exists
}
func GetScopeEndpoint(scope string) (Endpoint, bool) {
endpoint, exists := scopeToEndpoint[scope]
return endpoint, exists
}
================================================
FILE: pkg/analyzer/analyzers/analyzers.go
================================================
package analyzers
import (
"bytes"
"encoding/json"
"io"
"net/http"
"sort"
"github.com/fatih/color"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
type (
Analyzer interface {
Type() AnalyzerType
Analyze(ctx context.Context, credentialInfo map[string]string) (*AnalyzerResult, error)
}
AnalyzerType int
// AnalyzerResult is the output of analysis.
AnalyzerResult struct {
AnalyzerType AnalyzerType
Bindings []Binding
UnboundedResources []Resource
Metadata map[string]any
}
Resource struct {
Name string
FullyQualifiedName string
Type string
Metadata map[string]any
Parent *Resource
}
Permission struct {
Value string
Parent *Permission
}
Binding struct {
Resource Resource
Permission Permission
Condition string
}
)
type PermissionType string
const (
READ PermissionType = "Read"
WRITE PermissionType = "Write"
READ_WRITE PermissionType = "Read & Write"
NONE PermissionType = "None"
ERROR PermissionType = "Error"
FullAccess string = "full_access"
)
const (
AnalyzerTypeInvalid AnalyzerType = iota
AnalyzerTypeAirbrake
AnalyzerAnthropic
AnalyzerTypeAsana
AnalyzerTypeBitbucket
AnalyzerTypeDockerHub
AnalyzerTypeElevenLabs
AnalyzerTypeGitHub
AnalyzerTypeGitLab
AnalyzerTypeHuggingFace
AnalyzerTypeMailchimp
AnalyzerTypeMailgun
AnalyzerTypeMySQL
AnalyzerTypeOpenAI
AnalyzerTypeOpsgenie
AnalyzerTypePostgres
AnalyzerTypePostman
AnalyzerTypeSendgrid
AnalyzerTypeShopify
AnalyzerTypeSlack
AnalyzerTypeSourcegraph
AnalyzerTypeSquare
AnalyzerTypeStripe
AnalyzerTypeTwilio
AnalyzerTypePrivateKey
AnalyzerTypeNotion
AnalyzerTypeDigitalOcean
AnalyzerTypePlanetScale
AnalyzerTypeAirtableOAuth
AnalyzerTypeAirtablePat
AnalyzerTypeGroq
AnalyzerTypeLaunchDarkly
AnalyzerTypeFigma
AnalyzerTypePlaid
AnalyzerTypeNetlify
AnalyzerTypeFastly
AnalyzerTypeMonday
AnalyzerTypeDatadog
AnalyzerTypeNgrok
AnalyzerTypeMux
AnalyzerTypePosthog
AnalyzerTypeDropbox
AnalyzerTypeDataBricks
AnalyzerTypeJira
// Add new items here with AnalyzerType prefix
)
// analyzerTypeStrings maps the enum to its string representation.
var analyzerTypeStrings = map[AnalyzerType]string{
AnalyzerTypeInvalid: "Invalid",
AnalyzerTypeAirbrake: "Airbrake",
AnalyzerAnthropic: "Anthropic",
AnalyzerTypeAsana: "Asana",
AnalyzerTypeBitbucket: "Bitbucket",
AnalyzerTypeDigitalOcean: "DigitalOcean",
AnalyzerTypeDockerHub: "DockerHub",
AnalyzerTypeElevenLabs: "ElevenLabs",
AnalyzerTypeGitHub: "GitHub",
AnalyzerTypeGitLab: "GitLab",
AnalyzerTypeHuggingFace: "HuggingFace",
AnalyzerTypeMailchimp: "Mailchimp",
AnalyzerTypeMailgun: "Mailgun",
AnalyzerTypeMySQL: "MySQL",
AnalyzerTypeOpenAI: "OpenAI",
AnalyzerTypeOpsgenie: "Opsgenie",
AnalyzerTypePostgres: "Postgres",
AnalyzerTypePostman: "Postman",
AnalyzerTypeSendgrid: "Sendgrid",
AnalyzerTypeShopify: "Shopify",
AnalyzerTypeSlack: "Slack",
AnalyzerTypeSourcegraph: "Sourcegraph",
AnalyzerTypeSquare: "Square",
AnalyzerTypeStripe: "Stripe",
AnalyzerTypeTwilio: "Twilio",
AnalyzerTypePrivateKey: "PrivateKey",
AnalyzerTypeNotion: "Notion",
AnalyzerTypePlanetScale: "PlanetScale",
AnalyzerTypeAirtableOAuth: "AirtableOAuth",
AnalyzerTypeAirtablePat: "AirtablePat",
AnalyzerTypeGroq: "Groq",
AnalyzerTypeLaunchDarkly: "LaunchDarkly",
AnalyzerTypeFigma: "Figma",
AnalyzerTypePlaid: "Plaid",
AnalyzerTypeNetlify: "Netlify",
AnalyzerTypeFastly: "Fastly",
AnalyzerTypeMonday: "Monday",
AnalyzerTypeDatadog: "Datadog",
AnalyzerTypeNgrok: "Ngrok",
AnalyzerTypeMux: "Mux",
AnalyzerTypePosthog: "Posthog",
AnalyzerTypeDropbox: "Dropbox",
AnalyzerTypeDataBricks: "DataBricks",
AnalyzerTypeJira: "Jira",
// Add new mappings here
}
// String method to get the string representation of an AnalyzerType.
func (a AnalyzerType) String() string {
if str, ok := analyzerTypeStrings[a]; ok {
return str
}
return "Unknown"
}
// AvailableAnalyzers returns a sorted slice of AnalyzerType strings, skipping "Invalid".
func AvailableAnalyzers() []string {
var analyzerStrings []string
// Iterate through the map to collect all string values except "Invalid".
for typ, str := range analyzerTypeStrings {
if typ != AnalyzerTypeInvalid {
analyzerStrings = append(analyzerStrings, str)
}
}
// Sort the slice alphabetically.
sort.Strings(analyzerStrings)
return analyzerStrings
}
type PermissionStatus struct {
Value bool
IsError bool
}
type HttpStatusTest struct {
URL string
Method string
Payload map[string]interface{}
Params map[string]string
Valid []int
Invalid []int
Type PermissionType
Status PermissionStatus
Risk string
}
func (h *HttpStatusTest) RunTest(headers map[string]string) error {
// If body data, marshal to JSON
var data io.Reader
if h.Payload != nil {
jsonData, err := json.Marshal(h.Payload)
if err != nil {
return err
}
data = bytes.NewBuffer(jsonData)
}
// Create new HTTP request
client := &http.Client{}
req, err := http.NewRequest(h.Method, h.URL, data)
if err != nil {
return err
}
// Add custom headers if provided
for key, value := range headers {
req.Header.Set(key, value)
}
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Check response status code
switch {
case StatusContains(resp.StatusCode, h.Valid):
h.Status.Value = true
case StatusContains(resp.StatusCode, h.Invalid):
h.Status.Value = false
default:
h.Status.IsError = true
}
return nil
}
type Scope struct {
Name string
Tests []interface{}
}
func StatusContains(status int, vals []int) bool {
for _, v := range vals {
if status == v {
return true
}
}
return false
}
func GetWriterFromStatus(status PermissionType) func(a ...interface{}) string {
switch status {
case READ:
return color.New(color.FgYellow).SprintFunc()
case WRITE:
return color.New(color.FgGreen).SprintFunc()
case READ_WRITE:
return color.New(color.FgGreen).SprintFunc()
case NONE:
return color.New().SprintFunc()
case ERROR:
return color.New(color.FgRed).SprintFunc()
default:
return color.New().SprintFunc()
}
}
var GreenWriter = color.New(color.FgGreen).SprintFunc()
var YellowWriter = color.New(color.FgYellow).SprintFunc()
var RedWriter = color.New(color.FgRed).SprintFunc()
var DefaultWriter = color.New().SprintFunc()
// BindAllPermissions creates a Binding for each permission to the given
// resource.
func BindAllPermissions(r Resource, perms ...Permission) []Binding {
bindings := make([]Binding, len(perms))
for i, perm := range perms {
bindings[i] = Binding{
Resource: r,
Permission: perm,
}
}
return bindings
}
================================================
FILE: pkg/analyzer/analyzers/anthropic/anthropic.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go anthropic
package anthropic
import (
"errors"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
const (
// Key Types
APIKey = "API-Key"
AdminKey = "Admin-Key"
)
type Analyzer struct {
Cfg *config.Config
}
// SecretInfo hold the information about the anthropic key
type SecretInfo struct {
Valid bool
Type string // key type - TODO: Handle Anthropic Admin Keys
AnthropicResources []AnthropicResource
Permissions string // always full_access
Misc map[string]string
}
// AnthropicResource is any resource that can be accessed with anthropic key
type AnthropicResource struct {
ID string
Name string
Type string
Parent *AnthropicResource
Metadata map[string]string
}
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerAnthropic
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, exist := credInfo["key"]
if !exist {
return nil, errors.New("key not found in credentials info")
}
secretInfo, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(secretInfo), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
// just print the error in cli and continue as a partial success
color.Red("[x] Error : %s", err.Error())
}
if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}
if info.Valid {
color.Green("[!] Valid Anthropic %s\n\n", info.Type)
// no user information
// print full access permission
printPermission(info.Permissions)
// print resources
printAnthropicResources(info.AnthropicResources)
color.Yellow("\n[i] Expires: Never")
}
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
// create a HTTP client
client := analyzers.NewAnalyzeClient(cfg)
keyType := getKeyType(key)
var secretInfo = &SecretInfo{
Type: keyType,
}
switch keyType {
case APIKey:
if err := captureAPIKeyResources(client, key, secretInfo); err != nil {
return nil, err
}
case AdminKey:
if err := captureAdminKeyResources(client, key, secretInfo); err != nil {
return nil, err
}
default:
return nil, errors.New("unsupported key type")
}
// anthropic key has full access only
secretInfo.Permissions = PermissionStrings[FullAccess]
secretInfo.Valid = true
return secretInfo, nil
}
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerAnthropic,
Metadata: map[string]any{"Valid_Key": info.Valid},
Bindings: make([]analyzers.Binding, 0, len(info.AnthropicResources)), // pre-allocate with zero length
}
// extract information to create bindings and append to result bindings
for _, Anthropicresource := range info.AnthropicResources {
binding := analyzers.Binding{
Resource: analyzers.Resource{
Name: Anthropicresource.Name,
FullyQualifiedName: Anthropicresource.ID,
Type: Anthropicresource.Type,
Metadata: map[string]any{},
},
Permission: analyzers.Permission{
Value: info.Permissions,
},
}
if Anthropicresource.Parent != nil {
binding.Resource.Parent = &analyzers.Resource{
Name: Anthropicresource.Parent.Name,
FullyQualifiedName: Anthropicresource.Parent.ID,
Type: Anthropicresource.Parent.Type,
}
}
for key, value := range Anthropicresource.Metadata {
binding.Resource.Metadata[key] = value
}
result.Bindings = append(result.Bindings, binding)
}
return &result
}
func printPermission(permission string) {
color.Yellow("[i] Permissions:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Permission"})
t.AppendRow(table.Row{color.GreenString(permission)})
t.Render()
}
func printAnthropicResources(resources []AnthropicResource) {
color.Green("\n[i] Resources:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Resource Type", "Resource ID", "Resource Name"})
for _, resource := range resources {
t.AppendRow(table.Row{color.GreenString(resource.Type), color.GreenString(resource.ID), color.GreenString(resource.Name)})
}
t.Render()
}
// getKeyType return the type of key
func getKeyType(key string) string {
if strings.Contains(key, "sk-ant-admin01") {
return AdminKey
} else if strings.Contains(key, "sk-ant-api03") {
return APIKey
}
return ""
}
================================================
FILE: pkg/analyzer/analyzers/anthropic/anthropic_test.go
================================================
package anthropic
import (
_ "embed"
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed result_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*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("ANTHROPIC")
tests := []struct {
name string
secret string
want []byte // JSON string
wantErr bool
}{
{
name: "valid anthropic key",
secret: secret,
want: expectedOutput,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.secret})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/anthropic/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package anthropic
import "errors"
type Permission int
const (
Invalid Permission = iota
FullAccess Permission = iota
)
var (
PermissionStrings = map[Permission]string{
FullAccess: "full_access",
}
StringToPermission = map[string]Permission{
"full_access": FullAccess,
}
PermissionIDs = map[Permission]int{
FullAccess: 1,
}
IdToPermission = map[int]Permission{
1: FullAccess,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/anthropic/permissions.yaml
================================================
permissions:
- full_access
================================================
FILE: pkg/analyzer/analyzers/anthropic/requests.go
================================================
package anthropic
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
var endpoints = map[string]string{
// api key endpoints
"models": "https://api.anthropic.com/v1/models",
"messageBatches": "https://api.anthropic.com/v1/messages/batches",
// admin key endpoints
"orgUsers": "https://api.anthropic.com/v1/organizations/users",
"workspaces": "https://api.anthropic.com/v1/organizations/workspaces",
"workspaceMembers": "https://api.anthropic.com/v1/organizations/workspaces/%s/members", // require workspace id
"apiKeys": "https://api.anthropic.com/v1/organizations/api_keys",
}
type ModelsResponse struct {
Data []struct {
ID string `json:"id"`
DisplayName string `json:"display_name"`
Type string `json:"type"`
} `json:"data"`
}
type MessageResponse struct {
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
ProcessingStatus string `json:"processing_status"`
ExpiresAt string `json:"expires_at"`
ResultsURL string `json:"results_url"`
} `json:"data"`
}
type OrgUsersResponse struct {
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Email string `json:"email"`
Name string `json:"name"`
Role string `json:"role"`
} `json:"data"`
}
type WorkspacesResponse struct {
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
} `json:"data"`
}
type WorkspaceMembersResponse struct {
Data []struct {
WorkspaceID string `json:"workspace_id"`
UserID string `json:"user_id"`
Type string `json:"type"`
WorkspaceRole string `json:"workspace_role"`
} `json:"data"`
}
type APIKeysResponse struct {
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
WorkspaceID string `json:"workspace_id"`
CreatedBy struct {
ID string `json:"id"`
} `json:"created_by"`
PartialKeyHint string `json:"partial_key_hint"`
Status string `json:"status"`
} `json:"data"`
}
// makeAnthropicRequest send the API request to passed url with passed key as API Key and return response body and status code
func makeAnthropicRequest(client *http.Client, url, key string) ([]byte, int, error) {
// create request
req, err := http.NewRequest(http.MethodGet, url, http.NoBody)
if err != nil {
return nil, 0, err
}
// add required keys in the header
req.Header.Set("x-api-key", key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("anthropic-version", "2023-06-01")
resp, err := client.Do(req)
if err != nil {
return nil, 0, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
responseBodyByte, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, err
}
return responseBodyByte, resp.StatusCode, nil
}
// captureAPIKeyResources capture resources associated with api key
func captureAPIKeyResources(client *http.Client, apiKey string, secretInfo *SecretInfo) error {
if err := captureModels(client, apiKey, secretInfo); err != nil {
return err
}
if err := captureMessageBatches(client, apiKey, secretInfo); err != nil {
return err
}
return nil
}
// captureAdminKeyResources capture resources associated with admin key
func captureAdminKeyResources(client *http.Client, adminKey string, secretInfo *SecretInfo) error {
if err := captureOrgUsers(client, adminKey, secretInfo); err != nil {
return err
}
if err := captureWorkspaces(client, adminKey, secretInfo); err != nil {
return err
}
if err := captureAPIKeys(client, adminKey, secretInfo); err != nil {
return err
}
return nil
}
func captureModels(client *http.Client, apiKey string, secretInfo *SecretInfo) error {
response, statusCode, err := makeAnthropicRequest(client, endpoints["models"], apiKey)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var models ModelsResponse
if err := json.Unmarshal(response, &models); err != nil {
return err
}
for _, model := range models.Data {
secretInfo.AnthropicResources = append(secretInfo.AnthropicResources, AnthropicResource{
ID: model.ID,
Name: model.DisplayName,
Type: model.Type,
})
}
return nil
case http.StatusNotFound, http.StatusUnauthorized:
return fmt.Errorf("invalid/revoked api-key")
default:
return fmt.Errorf("unexpected status code: %d while fetching models", statusCode)
}
}
func captureMessageBatches(client *http.Client, apiKey string, secretInfo *SecretInfo) error {
response, statusCode, err := makeAnthropicRequest(client, endpoints["messageBatches"], apiKey)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var messageBatches MessageResponse
if err := json.Unmarshal(response, &messageBatches); err != nil {
return err
}
for _, messageBatch := range messageBatches.Data {
secretInfo.AnthropicResources = append(secretInfo.AnthropicResources, AnthropicResource{
ID: messageBatch.ID,
Name: "", // no name
Type: messageBatch.Type,
Metadata: map[string]string{
"expires_at": messageBatch.ExpiresAt,
"results_url": messageBatch.ResultsURL,
},
})
}
return nil
case http.StatusNotFound, http.StatusUnauthorized:
return fmt.Errorf("invalid/revoked api-key")
default:
return fmt.Errorf("unexpected status code: %d while fetching models", statusCode)
}
}
func captureOrgUsers(client *http.Client, adminKey string, secretInfo *SecretInfo) error {
response, statusCode, err := makeAnthropicRequest(client, endpoints["orgUsers"], adminKey)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var users OrgUsersResponse
if err := json.Unmarshal(response, &users); err != nil {
return err
}
for _, user := range users.Data {
secretInfo.AnthropicResources = append(secretInfo.AnthropicResources, AnthropicResource{
ID: user.ID,
Name: user.Name,
Type: user.Type,
Metadata: map[string]string{
"Role": user.Role,
"Email": user.Email,
},
})
}
return nil
case http.StatusNotFound, http.StatusUnauthorized:
return fmt.Errorf("invalid/revoked api-key")
default:
return fmt.Errorf("unexpected status code: %d while fetching models", statusCode)
}
}
func captureWorkspaces(client *http.Client, adminKey string, secretInfo *SecretInfo) error {
response, statusCode, err := makeAnthropicRequest(client, endpoints["workspaces"], adminKey)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var workspaces WorkspacesResponse
if err := json.Unmarshal(response, &workspaces); err != nil {
return err
}
for _, workspace := range workspaces.Data {
resource := AnthropicResource{
ID: workspace.ID,
Name: workspace.Name,
Type: workspace.Type,
}
secretInfo.AnthropicResources = append(secretInfo.AnthropicResources, resource)
// capture each workspace members
if err := captureWorkspaceMembers(client, adminKey, resource, secretInfo); err != nil {
return err
}
}
return nil
case http.StatusNotFound, http.StatusUnauthorized:
return fmt.Errorf("invalid/revoked api-key")
default:
return fmt.Errorf("unexpected status code: %d while fetching models", statusCode)
}
}
func captureWorkspaceMembers(client *http.Client, key string, parentWorkspace AnthropicResource, secretInfo *SecretInfo) error {
response, statusCode, err := makeAnthropicRequest(client, fmt.Sprintf(endpoints["workspaceMembers"], parentWorkspace.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var members WorkspaceMembersResponse
if err := json.Unmarshal(response, &members); err != nil {
return err
}
for _, member := range members.Data {
secretInfo.AnthropicResources = append(secretInfo.AnthropicResources, AnthropicResource{
ID: fmt.Sprintf("anthropic/workspace/%s/member/%s", member.WorkspaceID, member.UserID),
Name: member.UserID,
Type: member.Type,
Parent: &parentWorkspace,
})
}
return nil
case http.StatusNotFound, http.StatusUnauthorized:
return fmt.Errorf("invalid/revoked api-key")
default:
return fmt.Errorf("unexpected status code: %d while fetching models", statusCode)
}
}
func captureAPIKeys(client *http.Client, adminKey string, secretInfo *SecretInfo) error {
response, statusCode, err := makeAnthropicRequest(client, endpoints["apiKeys"], adminKey)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var apiKeys APIKeysResponse
if err := json.Unmarshal(response, &apiKeys); err != nil {
return err
}
for _, apiKey := range apiKeys.Data {
secretInfo.AnthropicResources = append(secretInfo.AnthropicResources, AnthropicResource{
ID: apiKey.ID,
Name: apiKey.Name,
Type: apiKey.Type,
Metadata: map[string]string{
"WorkspaceID": apiKey.WorkspaceID,
"CreatedBy": apiKey.CreatedBy.ID,
"PartialKeyHint": apiKey.PartialKeyHint,
"Status": apiKey.Status,
},
})
}
return nil
case http.StatusNotFound, http.StatusUnauthorized:
return fmt.Errorf("invalid/revoked api-key")
default:
return fmt.Errorf("unexpected status code: %d while fetching models", statusCode)
}
}
================================================
FILE: pkg/analyzer/analyzers/anthropic/result_output.json
================================================
{
"AnalyzerType": 2,
"Bindings": [
{
"Resource": {
"Name": "Claude Sonnet 4.6",
"FullyQualifiedName": "claude-sonnet-4-6",
"Type": "model",
"Metadata": {},
"Parent": null
},
"Permission": { "Value": "full_access", "Parent": null },
"Condition": ""
},
{
"Resource": {
"Name": "Claude Opus 4.6",
"FullyQualifiedName": "claude-opus-4-6",
"Type": "model",
"Metadata": {},
"Parent": null
},
"Permission": { "Value": "full_access", "Parent": null },
"Condition": ""
},
{
"Resource": {
"Name": "Claude Opus 4.5",
"FullyQualifiedName": "claude-opus-4-5-20251101",
"Type": "model",
"Metadata": {},
"Parent": null
},
"Permission": { "Value": "full_access", "Parent": null },
"Condition": ""
},
{
"Resource": {
"Name": "Claude Haiku 4.5",
"FullyQualifiedName": "claude-haiku-4-5-20251001",
"Type": "model",
"Metadata": {},
"Parent": null
},
"Permission": { "Value": "full_access", "Parent": null },
"Condition": ""
},
{
"Resource": {
"Name": "Claude Sonnet 4.5",
"FullyQualifiedName": "claude-sonnet-4-5-20250929",
"Type": "model",
"Metadata": {},
"Parent": null
},
"Permission": { "Value": "full_access", "Parent": null },
"Condition": ""
},
{
"Resource": {
"Name": "Claude Opus 4.1",
"FullyQualifiedName": "claude-opus-4-1-20250805",
"Type": "model",
"Metadata": {},
"Parent": null
},
"Permission": { "Value": "full_access", "Parent": null },
"Condition": ""
},
{
"Resource": {
"Name": "Claude Opus 4",
"FullyQualifiedName": "claude-opus-4-20250514",
"Type": "model",
"Metadata": {},
"Parent": null
},
"Permission": { "Value": "full_access", "Parent": null },
"Condition": ""
},
{
"Resource": {
"Name": "Claude Sonnet 4",
"FullyQualifiedName": "claude-sonnet-4-20250514",
"Type": "model",
"Metadata": {},
"Parent": null
},
"Permission": { "Value": "full_access", "Parent": null },
"Condition": ""
},
{
"Resource": {
"Name": "Claude Sonnet 3.7",
"FullyQualifiedName": "claude-3-7-sonnet-20250219",
"Type": "model",
"Metadata": {},
"Parent": null
},
"Permission": { "Value": "full_access", "Parent": null },
"Condition": ""
},
{
"Resource": {
"Name": "Claude Haiku 3.5",
"FullyQualifiedName": "claude-3-5-haiku-20241022",
"Type": "model",
"Metadata": {},
"Parent": null
},
"Permission": { "Value": "full_access", "Parent": null },
"Condition": ""
},
{
"Resource": {
"Name": "Claude Haiku 3",
"FullyQualifiedName": "claude-3-haiku-20240307",
"Type": "model",
"Metadata": {},
"Parent": null
},
"Permission": { "Value": "full_access", "Parent": null },
"Condition": ""
},
{
"Resource": {
"Name": "",
"FullyQualifiedName": "msgbatch_015FDqbx29LDeVvbwwyCe314",
"Type": "message_batch",
"Metadata": {
"expires_at": "2025-02-05T07:36:34.761695+00:00",
"results_url": ""
},
"Parent": null
},
"Permission": { "Value": "full_access", "Parent": null },
"Condition": ""
}
],
"UnboundedResources": null,
"Metadata": { "Valid_Key": true }
}
================================================
FILE: pkg/analyzer/analyzers/asana/asana.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go asana
package asana
// ToDo: Add OAuth token support.
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeAsana }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, errors.New("key not found in credInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{}
// resources/permission setup
permissions := allPermissions()
userResource := analyzers.Resource{
Name: info.Data.Name,
FullyQualifiedName: info.Data.ID,
Type: "user",
Metadata: map[string]any{
"email": info.Data.Email,
"type": info.Data.Type,
},
}
// bindings to all permissions to resources
bindings := analyzers.BindAllPermissions(userResource, permissions...)
result.Bindings = append(result.Bindings, bindings...)
// unbounded resources
result.UnboundedResources = make([]analyzers.Resource, 0, len(info.Data.Workspaces))
for _, workspace := range info.Data.Workspaces {
resource := analyzers.Resource{
Name: workspace.Name,
FullyQualifiedName: workspace.ID,
Type: "workspace",
}
result.UnboundedResources = append(result.UnboundedResources, resource)
}
return &result
}
type SecretInfo struct {
Data struct {
ID string `json:"gid"`
Email string `json:"email"`
Name string `json:"name"`
Type string `json:"resource_type"`
Workspaces []struct {
ID string `json:"gid"`
Name string `json:"name"`
} `json:"workspaces"`
} `json:"data"`
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
me, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] %s", err.Error())
return
}
printMetadata(me)
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
var me SecretInfo
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://app.asana.com/api/1.0/users/me", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("Invalid Asana API Key")
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&me)
if err != nil {
return nil, err
}
if me.Data.Email == "" {
return nil, fmt.Errorf("Invalid Asana API Key")
}
return &me, nil
}
func printMetadata(me *SecretInfo) {
color.Green("[!] Valid Asana API Key\n\n")
color.Yellow("[i] User Information")
color.Yellow(" Name: %s", me.Data.Name)
color.Yellow(" Email: %s", me.Data.Email)
color.Yellow(" Type: %s\n\n", me.Data.Type)
color.Green("[i] Permissions: Full Access\n\n")
color.Yellow("[i] Accessible Workspaces")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Workspace Name"})
for _, workspace := range me.Data.Workspaces {
t.AppendRow(table.Row{color.GreenString(workspace.Name)})
}
t.Render()
}
func allPermissions() []analyzers.Permission {
permissions := make([]analyzers.Permission, 0, len(PermissionStrings))
for _, permission := range PermissionStrings {
permissions = append(permissions, analyzers.Permission{
Value: permission,
})
}
return permissions
}
================================================
FILE: pkg/analyzer/analyzers/asana/asana_test.go
================================================
package asana
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid Asana OAUTH Token",
key: testSecrets.MustGetField("ASANAOAUTH_TOKEN"),
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/asana/expected_output.json
================================================
{"AnalyzerType":0,"Bindings":[{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"autdit_logs:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"portfolios:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"sections:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"tasks:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"user_task_lists:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"user_task_lists:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"autdit_logs:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"jobs:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"portfolios:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"project_memberships:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"project_memberships:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"users:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"users:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"memberships:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"custom_fields:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"goals:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"jobs:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"tags:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"teams:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"teams:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"custom_field_settings:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"projects:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"sections:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"allocations:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"custom_fields:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"projects:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"allocations:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"custom_field_settings:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"batch_api:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"events:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"tasks:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"rules:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"batch_api:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"goals:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"tags:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"memberships:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"attachments:read","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"attachments:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"events:write","Parent":null}},{"Resource":{"Name":"rendyplayground","FullyQualifiedName":"1200552284974896","Type":"user","Metadata":{"email":"rendyplayground@gmail.com","type":"user"},"Parent":null},"Permission":{"Value":"rules:write","Parent":null}}],"UnboundedResources":[{"Name":"Design","FullyQualifiedName":"1200552201649567","Type":"workspace","Metadata":null,"Parent":null}],"Metadata":null}
================================================
FILE: pkg/analyzer/analyzers/asana/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package asana
import "errors"
type Permission int
const (
Invalid Permission = iota
AllocationsRead Permission = iota
AllocationsWrite Permission = iota
AttachmentsRead Permission = iota
AttachmentsWrite Permission = iota
AutditLogsRead Permission = iota
AutditLogsWrite Permission = iota
CustomFieldsRead Permission = iota
CustomFieldsWrite Permission = iota
CustomFieldSettingsRead Permission = iota
CustomFieldSettingsWrite Permission = iota
BatchApiRead Permission = iota
BatchApiWrite Permission = iota
EventsRead Permission = iota
EventsWrite Permission = iota
GoalsRead Permission = iota
GoalsWrite Permission = iota
JobsRead Permission = iota
JobsWrite Permission = iota
PortfoliosRead Permission = iota
PortfoliosWrite Permission = iota
ProjectsRead Permission = iota
ProjectsWrite Permission = iota
ProjectMembershipsRead Permission = iota
ProjectMembershipsWrite Permission = iota
SectionsRead Permission = iota
SectionsWrite Permission = iota
TagsRead Permission = iota
TagsWrite Permission = iota
TasksRead Permission = iota
TasksWrite Permission = iota
TeamsRead Permission = iota
TeamsWrite Permission = iota
UsersRead Permission = iota
UsersWrite Permission = iota
UserTaskListsRead Permission = iota
UserTaskListsWrite Permission = iota
MembershipsRead Permission = iota
MembershipsWrite Permission = iota
RulesRead Permission = iota
RulesWrite Permission = iota
)
var (
PermissionStrings = map[Permission]string{
AllocationsRead: "allocations:read",
AllocationsWrite: "allocations:write",
AttachmentsRead: "attachments:read",
AttachmentsWrite: "attachments:write",
AutditLogsRead: "autdit_logs:read",
AutditLogsWrite: "autdit_logs:write",
CustomFieldsRead: "custom_fields:read",
CustomFieldsWrite: "custom_fields:write",
CustomFieldSettingsRead: "custom_field_settings:read",
CustomFieldSettingsWrite: "custom_field_settings:write",
BatchApiRead: "batch_api:read",
BatchApiWrite: "batch_api:write",
EventsRead: "events:read",
EventsWrite: "events:write",
GoalsRead: "goals:read",
GoalsWrite: "goals:write",
JobsRead: "jobs:read",
JobsWrite: "jobs:write",
PortfoliosRead: "portfolios:read",
PortfoliosWrite: "portfolios:write",
ProjectsRead: "projects:read",
ProjectsWrite: "projects:write",
ProjectMembershipsRead: "project_memberships:read",
ProjectMembershipsWrite: "project_memberships:write",
SectionsRead: "sections:read",
SectionsWrite: "sections:write",
TagsRead: "tags:read",
TagsWrite: "tags:write",
TasksRead: "tasks:read",
TasksWrite: "tasks:write",
TeamsRead: "teams:read",
TeamsWrite: "teams:write",
UsersRead: "users:read",
UsersWrite: "users:write",
UserTaskListsRead: "user_task_lists:read",
UserTaskListsWrite: "user_task_lists:write",
MembershipsRead: "memberships:read",
MembershipsWrite: "memberships:write",
RulesRead: "rules:read",
RulesWrite: "rules:write",
}
StringToPermission = map[string]Permission{
"allocations:read": AllocationsRead,
"allocations:write": AllocationsWrite,
"attachments:read": AttachmentsRead,
"attachments:write": AttachmentsWrite,
"autdit_logs:read": AutditLogsRead,
"autdit_logs:write": AutditLogsWrite,
"custom_fields:read": CustomFieldsRead,
"custom_fields:write": CustomFieldsWrite,
"custom_field_settings:read": CustomFieldSettingsRead,
"custom_field_settings:write": CustomFieldSettingsWrite,
"batch_api:read": BatchApiRead,
"batch_api:write": BatchApiWrite,
"events:read": EventsRead,
"events:write": EventsWrite,
"goals:read": GoalsRead,
"goals:write": GoalsWrite,
"jobs:read": JobsRead,
"jobs:write": JobsWrite,
"portfolios:read": PortfoliosRead,
"portfolios:write": PortfoliosWrite,
"projects:read": ProjectsRead,
"projects:write": ProjectsWrite,
"project_memberships:read": ProjectMembershipsRead,
"project_memberships:write": ProjectMembershipsWrite,
"sections:read": SectionsRead,
"sections:write": SectionsWrite,
"tags:read": TagsRead,
"tags:write": TagsWrite,
"tasks:read": TasksRead,
"tasks:write": TasksWrite,
"teams:read": TeamsRead,
"teams:write": TeamsWrite,
"users:read": UsersRead,
"users:write": UsersWrite,
"user_task_lists:read": UserTaskListsRead,
"user_task_lists:write": UserTaskListsWrite,
"memberships:read": MembershipsRead,
"memberships:write": MembershipsWrite,
"rules:read": RulesRead,
"rules:write": RulesWrite,
}
PermissionIDs = map[Permission]int{
AllocationsRead: 1,
AllocationsWrite: 2,
AttachmentsRead: 3,
AttachmentsWrite: 4,
AutditLogsRead: 5,
AutditLogsWrite: 6,
CustomFieldsRead: 7,
CustomFieldsWrite: 8,
CustomFieldSettingsRead: 9,
CustomFieldSettingsWrite: 10,
BatchApiRead: 11,
BatchApiWrite: 12,
EventsRead: 13,
EventsWrite: 14,
GoalsRead: 15,
GoalsWrite: 16,
JobsRead: 17,
JobsWrite: 18,
PortfoliosRead: 19,
PortfoliosWrite: 20,
ProjectsRead: 21,
ProjectsWrite: 22,
ProjectMembershipsRead: 23,
ProjectMembershipsWrite: 24,
SectionsRead: 25,
SectionsWrite: 26,
TagsRead: 27,
TagsWrite: 28,
TasksRead: 29,
TasksWrite: 30,
TeamsRead: 31,
TeamsWrite: 32,
UsersRead: 33,
UsersWrite: 34,
UserTaskListsRead: 35,
UserTaskListsWrite: 36,
MembershipsRead: 37,
MembershipsWrite: 38,
RulesRead: 39,
RulesWrite: 40,
}
IdToPermission = map[int]Permission{
1: AllocationsRead,
2: AllocationsWrite,
3: AttachmentsRead,
4: AttachmentsWrite,
5: AutditLogsRead,
6: AutditLogsWrite,
7: CustomFieldsRead,
8: CustomFieldsWrite,
9: CustomFieldSettingsRead,
10: CustomFieldSettingsWrite,
11: BatchApiRead,
12: BatchApiWrite,
13: EventsRead,
14: EventsWrite,
15: GoalsRead,
16: GoalsWrite,
17: JobsRead,
18: JobsWrite,
19: PortfoliosRead,
20: PortfoliosWrite,
21: ProjectsRead,
22: ProjectsWrite,
23: ProjectMembershipsRead,
24: ProjectMembershipsWrite,
25: SectionsRead,
26: SectionsWrite,
27: TagsRead,
28: TagsWrite,
29: TasksRead,
30: TasksWrite,
31: TeamsRead,
32: TeamsWrite,
33: UsersRead,
34: UsersWrite,
35: UserTaskListsRead,
36: UserTaskListsWrite,
37: MembershipsRead,
38: MembershipsWrite,
39: RulesRead,
40: RulesWrite,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/asana/permissions.yaml
================================================
permissions:
- allocations:read
- allocations:write
- attachments:read
- attachments:write
- autdit_logs:read
- autdit_logs:write
- custom_fields:read
- custom_fields:write
- custom_field_settings:read
- custom_field_settings:write
- batch_api:read
- batch_api:write
- events:read
- events:write
- goals:read
- goals:write
- jobs:read
- jobs:write
- portfolios:read
- portfolios:write
- projects:read
- projects:write
- project_memberships:read
- project_memberships:write
- sections:read
- sections:write
- tags:read
- tags:write
- tasks:read
- tasks:write
- teams:read
- teams:write
- users:read
- users:write
- user_task_lists:read
- user_task_lists:write
- memberships:read
- memberships:write
- rules:read
- rules:write
================================================
FILE: pkg/analyzer/analyzers/bitbucket/bitbucket.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go bitbucket
package bitbucket
import (
"encoding/json"
"errors"
"net/http"
"os"
"sort"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
var resource_name_map = map[string]string{
"repo_access_token": "Repository",
"project_access_token": "Project",
"workspace_access_token": "Workspace",
}
type SecretInfo struct {
Type string
OauthScopes []string
Repos []Repo
}
type Repo struct {
ID string `json:"uuid"`
FullName string `json:"full_name"`
RepoName string `json:"name"`
Project struct {
ID string `json:"uuid"`
Name string `json:"name"`
} `json:"project"`
Workspace struct {
ID string `json:"uuid"`
Name string `json:"name"`
} `json:"workspace"`
IsPrivate bool `json:"is_private"`
Owner struct {
ID string `json:"uuid"`
Username string `json:"username"`
} `json:"owner"`
Role string
}
type RepoJSON struct {
Values []Repo `json:"values"`
}
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeBitbucket }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, errors.New("key not found in credentialInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeBitbucket,
}
// add unbounded resources
result.UnboundedResources = make([]analyzers.Resource, len(info.Repos))
for i, repo := range info.Repos {
result.UnboundedResources[i] = analyzers.Resource{
Type: "repository",
Name: repo.FullName,
FullyQualifiedName: "bitbucket.com/repository/" + repo.ID,
Parent: &analyzers.Resource{
Type: "project",
Name: repo.Project.Name,
FullyQualifiedName: "bitbucket.com/project/" + repo.Project.ID,
Parent: &analyzers.Resource{
Type: "workspace",
Name: repo.Workspace.Name,
FullyQualifiedName: "bitbucket.com/workspace/" + repo.Workspace.ID,
},
},
Metadata: map[string]any{
"owner_id": repo.Owner.ID,
"owner": repo.Owner.Username,
"is_private": repo.IsPrivate,
"role": repo.Role,
},
}
}
credentialResource := &analyzers.Resource{
Type: info.Type,
Name: resource_name_map[info.Type],
FullyQualifiedName: "bitbucket.com/credential/" + info.Type,
Metadata: map[string]any{
"type": credential_type_map[info.Type],
},
}
for _, scope := range info.OauthScopes {
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: *credentialResource,
Permission: analyzers.Permission{
Value: scope,
},
})
}
return &result
}
func getScopesAndType(cfg *config.Config, key string) (string, []string, error) {
// client
client := analyzers.NewAnalyzeClient(cfg)
// request
req, err := http.NewRequest("GET", "https://api.bitbucket.org/2.0/repositories", nil)
if err != nil {
return "", nil, err
}
// headers
req.Header.Set("Authorization", "Bearer "+key)
// response
resp, err := client.Do(req)
if err != nil {
return "", nil, err
}
defer resp.Body.Close()
// parse response headers
credentialType := resp.Header.Get("x-credential-type")
oauthScopes := resp.Header.Get("x-oauth-scopes")
scopes := strings.Split(oauthScopes, ", ")
return credentialType, scopes, nil
}
func scopesToBitbucketScopes(scopes ...analyzers.Permission) []BitbucketScope {
scopesSlice := []BitbucketScope{}
for _, scope := range scopes {
scope := scope.Value
mapping := oauth_scope_map[scope]
for _, impliedScope := range mapping.ImpliedScopes {
scopesSlice = append(scopesSlice, oauth_scope_map[impliedScope])
}
scopesSlice = append(scopesSlice, oauth_scope_map[scope])
}
// sort scopes by category
sort.Sort(ByCategoryAndName(scopesSlice))
return scopesSlice
}
func getRepositories(cfg *config.Config, key string, role string) (RepoJSON, error) {
var repos RepoJSON
// client
client := analyzers.NewAnalyzeClient(cfg)
// request
req, err := http.NewRequest("GET", "https://api.bitbucket.org/2.0/repositories", nil)
if err != nil {
return repos, err
}
// headers
req.Header.Set("Authorization", "Bearer "+key)
// add query params
q := req.URL.Query()
q.Add("role", role)
q.Add("pagelen", "100")
req.URL.RawQuery = q.Encode()
// response
resp, err := client.Do(req)
if err != nil {
return repos, err
}
defer resp.Body.Close()
// parse response body
err = json.NewDecoder(resp.Body).Decode(&repos)
if err != nil {
return repos, err
}
return repos, nil
}
func getAllRepos(cfg *config.Config, key string) ([]Repo, error) {
roles := []string{"member", "contributor", "admin", "owner"}
var allRepos = make(map[string]Repo, 0)
for _, role := range roles {
repos, err := getRepositories(cfg, key, role)
if err != nil {
return nil, err
}
// purposefully overwriting, so that get the most permissive role
for _, repo := range repos.Values {
repo.Role = role
allRepos[repo.FullName] = repo
}
}
repoSlice := make([]Repo, 0, len(allRepos))
for _, repo := range allRepos {
repoSlice = append(repoSlice, repo)
}
return repoSlice, nil
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
credentialType, oauthScopes, err := getScopesAndType(cfg, key)
if err != nil {
return nil, err
}
// get all repos available to user
// ToDo: pagination
repos, err := getAllRepos(cfg, key)
if err != nil {
return nil, err
}
return &SecretInfo{
Type: credentialType,
OauthScopes: oauthScopes,
Repos: repos,
}, nil
}
func convertScopeToAnalyzerPermissions(scopes []string) []analyzers.Permission {
permissions := make([]analyzers.Permission, 0, len(scopes))
for _, scope := range scopes {
permissions = append(permissions, analyzers.Permission{Value: scope})
}
return permissions
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
printScopes(info.Type, convertScopeToAnalyzerPermissions(info.OauthScopes))
printAccessibleRepositories(info.Repos)
}
func printScopes(credentialType string, scopes []analyzers.Permission) {
if credentialType == "" {
color.Red("[x] Invalid Bitbucket access token.")
return
}
color.Green("[!] Valid Bitbucket access token.\n\n")
color.Green("[i] Credential Type: %s\n\n", credential_type_map[credentialType])
color.Yellow("[i] Access Token Scopes:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Category", "Permission"})
currentCategory := ""
for _, scope := range scopesToBitbucketScopes(scopes...) {
if currentCategory != scope.Category {
currentCategory = scope.Category
t.AppendRow([]any{scope.Category, ""})
}
t.AppendRow([]any{"", color.GreenString(scope.Name)})
}
t.Render()
}
func printAccessibleRepositories(repos []Repo) {
color.Yellow("\n[i] Accessible Repositories:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Repository", "Project", "Workspace", "Owner", "Is Private", "This User's Role"})
for _, repo := range repos {
private := ""
if repo.IsPrivate {
private = color.GreenString("Yes")
} else {
private = color.RedString("No")
}
t.AppendRow([]any{
color.GreenString(repo.RepoName),
color.GreenString(repo.Project.Name),
color.GreenString(repo.Workspace.Name),
color.GreenString(repo.Owner.Username),
private,
color.GreenString(repo.Role),
})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/bitbucket/bitbucket_test.go
================================================
package bitbucket
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(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)
}
tests := []struct {
name string
sid string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid Bitbucket key",
key: testSecrets.MustGetField("BITBUCKET_ANALYZE_TOKEN"),
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key, "sid": tt.sid})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = \n%s", gotIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/bitbucket/expected_output.json
================================================
{
"AnalyzerType": 3,
"Bindings": [
{
"Resource": {
"Name": "Repository",
"FullyQualifiedName": "bitbucket.com/credential/repo_access_token",
"Type": "repo_access_token",
"Metadata": {
"type": "Repository Access Token (Can access 1 repository)"
},
"Parent": null
},
"Permission": {
"Value": "pipeline",
"Parent": null
}
},
{
"Resource": {
"Name": "Repository",
"FullyQualifiedName": "bitbucket.com/credential/repo_access_token",
"Type": "repo_access_token",
"Metadata": {
"type": "Repository Access Token (Can access 1 repository)"
},
"Parent": null
},
"Permission": {
"Value": "pullrequest",
"Parent": null
}
},
{
"Resource": {
"Name": "Repository",
"FullyQualifiedName": "bitbucket.com/credential/repo_access_token",
"Type": "repo_access_token",
"Metadata": {
"type": "Repository Access Token (Can access 1 repository)"
},
"Parent": null
},
"Permission": {
"Value": "runner",
"Parent": null
}
},
{
"Resource": {
"Name": "Repository",
"FullyQualifiedName": "bitbucket.com/credential/repo_access_token",
"Type": "repo_access_token",
"Metadata": {
"type": "Repository Access Token (Can access 1 repository)"
},
"Parent": null
},
"Permission": {
"Value": "webhook",
"Parent": null
}
}
],
"UnboundedResources": [
{
"Name": "basit-trufflesec/repo1",
"FullyQualifiedName": "bitbucket.com/repository/{8961ef70-000c-47ca-9348-5f9ecee875d6}",
"Type": "repository",
"Metadata": {
"is_private": true,
"owner": "basit-trufflesec",
"owner_id": "{521b49b6-7709-484a-8aa8-ecc3a6da08eb}",
"role": "admin"
},
"Parent": {
"Name": "repo-analyzer",
"FullyQualifiedName": "bitbucket.com/project/{8a693e10-087f-41fc-ba67-2d1414ab1c86}",
"Type": "project",
"Metadata": null,
"Parent": {
"Name": "basit-trufflesec",
"FullyQualifiedName": "bitbucket.com/workspace/{521b49b6-7709-484a-8aa8-ecc3a6da08eb}",
"Type": "workspace",
"Metadata": null,
"Parent": null
}
}
}
],
"Metadata": null
}
================================================
FILE: pkg/analyzer/analyzers/bitbucket/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package bitbucket
import "errors"
type Permission int
const (
Invalid Permission = iota
Project Permission = iota
ProjectAdmin Permission = iota
Repository Permission = iota
RepositoryWrite Permission = iota
RepositoryAdmin Permission = iota
RepositoryDelete Permission = iota
Pullrequest Permission = iota
PullrequestWrite Permission = iota
Webhook Permission = iota
Account Permission = iota
Pipeline Permission = iota
PipelineWrite Permission = iota
PipelineVariable Permission = iota
Runner Permission = iota
RunnerWrite Permission = iota
)
var (
PermissionStrings = map[Permission]string{
Project: "project",
ProjectAdmin: "project:admin",
Repository: "repository",
RepositoryWrite: "repository:write",
RepositoryAdmin: "repository:admin",
RepositoryDelete: "repository:delete",
Pullrequest: "pullrequest",
PullrequestWrite: "pullrequest:write",
Webhook: "webhook",
Account: "account",
Pipeline: "pipeline",
PipelineWrite: "pipeline:write",
PipelineVariable: "pipeline:variable",
Runner: "runner",
RunnerWrite: "runner:write",
}
StringToPermission = map[string]Permission{
"project": Project,
"project:admin": ProjectAdmin,
"repository": Repository,
"repository:write": RepositoryWrite,
"repository:admin": RepositoryAdmin,
"repository:delete": RepositoryDelete,
"pullrequest": Pullrequest,
"pullrequest:write": PullrequestWrite,
"webhook": Webhook,
"account": Account,
"pipeline": Pipeline,
"pipeline:write": PipelineWrite,
"pipeline:variable": PipelineVariable,
"runner": Runner,
"runner:write": RunnerWrite,
}
PermissionIDs = map[Permission]int{
Project: 1,
ProjectAdmin: 2,
Repository: 3,
RepositoryWrite: 4,
RepositoryAdmin: 5,
RepositoryDelete: 6,
Pullrequest: 7,
PullrequestWrite: 8,
Webhook: 9,
Account: 10,
Pipeline: 11,
PipelineWrite: 12,
PipelineVariable: 13,
Runner: 14,
RunnerWrite: 15,
}
IdToPermission = map[int]Permission{
1: Project,
2: ProjectAdmin,
3: Repository,
4: RepositoryWrite,
5: RepositoryAdmin,
6: RepositoryDelete,
7: Pullrequest,
8: PullrequestWrite,
9: Webhook,
10: Account,
11: Pipeline,
12: PipelineWrite,
13: PipelineVariable,
14: Runner,
15: RunnerWrite,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/bitbucket/permissions.yaml
================================================
permissions:
- project
- project:admin
- repository
- repository:write
- repository:admin
- repository:delete
- pullrequest
- pullrequest:write
- webhook
- account
- pipeline
- pipeline:write
- pipeline:variable
- runner
- runner:write
================================================
FILE: pkg/analyzer/analyzers/bitbucket/scopes.go
================================================
package bitbucket
var credential_type_map = map[string]string{
"repo_access_token": "Repository Access Token (Can access 1 repository)",
"project_access_token": "Project Access Token (Can access all repos in 1 project)",
"workspace_access_token": "Workspace Access Token (Can access all projects and repos in 1 workspace)",
}
type BitbucketScope struct {
Name string `json:"name"`
Category string `json:"category"`
ImpliedScopes []string `json:"implied_scopes"`
}
type ByCategoryAndName []BitbucketScope
func (a ByCategoryAndName) Len() int { return len(a) }
func (a ByCategoryAndName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByCategoryAndName) Less(i, j int) bool {
categoryOrder := map[string]int{
"Account": 0,
"Projects": 1,
"Repositories": 2,
"Pull Requests": 3,
"Webhooks": 4,
"Pipelines": 5,
"Runners": 6,
}
nameOrder := map[string]int{
"Read": 0,
"Write": 1,
"Admin": 2,
"Delete": 3,
"Edit variables": 4,
"Read and write": 5,
}
if categoryOrder[a[i].Category] != categoryOrder[a[j].Category] {
return categoryOrder[a[i].Category] < categoryOrder[a[j].Category]
}
return nameOrder[a[i].Name] < nameOrder[a[j].Name]
}
var oauth_scope_map = map[string]BitbucketScope{
"repository": {
Name: "Read",
Category: "Repositories",
},
"repository:write": {
Name: "Write",
Category: "Repositories",
ImpliedScopes: []string{"repository"},
},
"repository:admin": {
Name: "Admin",
Category: "Repositories",
},
"repository:delete": {
Name: "Delete",
Category: "Repositories",
},
"pullrequest": {
Name: "Read",
Category: "Pull Requests",
ImpliedScopes: []string{"repository"},
},
"pullrequest:write": {
Name: "Write",
Category: "Pull Requests",
ImpliedScopes: []string{"pullrequest", "repository", "repository:write"},
},
"webhook": {
Name: "Read and write",
Category: "Webhooks",
},
"pipeline": {
Name: "Read",
Category: "Pipelines",
},
"pipeline:write": {
Name: "Write",
Category: "Pipelines",
ImpliedScopes: []string{"pipeline"},
},
"pipeline:variable": {
Name: "Edit variables",
Category: "Pipelines",
ImpliedScopes: []string{"pipeline", "pipeline:write"},
},
"runner": {
Name: "Read",
Category: "Runners",
},
"runner:write": {
Name: "Write",
Category: "Runners",
ImpliedScopes: []string{"runner"},
},
"project": {
Name: "Read",
Category: "Projects",
ImpliedScopes: []string{"repository"},
},
"project:admin": {
Name: "Admin",
Category: "Projects",
},
"account": {
Name: "Read",
Category: "Account",
},
}
================================================
FILE: pkg/analyzer/analyzers/client.go
================================================
package analyzers
import (
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"golang.org/x/time/rate"
)
type AnalyzeClient struct {
http.Client
LoggingEnabled bool
LogFile string
}
func CreateLogFileName(baseName string) string {
// Get the current time
currentTime := time.Now()
// Format the time as "2024_06_30_07_15_30"
timeString := currentTime.Format("2006_01_02_15_04_05")
// Create the log file name
logFileName := fmt.Sprintf("%s_%s.log", timeString, baseName)
return logFileName
}
type ClientOption func(*http.Client)
// This returns a client that is restricted and filters out unsafe requests returning a success status code.
func NewAnalyzeClient(cfg *config.Config, opts ...func(*http.Client)) *http.Client {
client := &http.Client{
Transport: AnalyzerRoundTripper{parent: http.DefaultTransport},
}
if cfg != nil && cfg.LoggingEnabled {
client = &http.Client{
Transport: LoggingRoundTripper{
parent: client.Transport,
logFile: cfg.LogFile,
},
}
}
for _, opt := range opts {
opt(client)
}
return client
}
// This returns a client that is unrestricted and does not filter out unsafe requests returning a success status code.
func NewAnalyzeClientUnrestricted(cfg *config.Config, opts ...ClientOption) *http.Client {
client := &http.Client{
Transport: http.DefaultTransport,
}
if cfg != nil && cfg.LoggingEnabled {
client = &http.Client{
Transport: LoggingRoundTripper{
parent: client.Transport,
logFile: cfg.LogFile,
},
}
}
for _, opt := range opts {
opt(client)
}
return client
}
type LoggingRoundTripper struct {
parent http.RoundTripper
// TODO: io.Writer
logFile string
}
func (r LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
startTime := time.Now()
resp, parentErr := r.parent.RoundTrip(req)
if resp == nil {
return resp, parentErr
}
// TODO: JSON
var logEntry string
if parentErr != nil {
logEntry = fmt.Sprintf("Date: %s, Method: %s, Path: %s, Status: %d, Error: %s\n",
startTime.Format(time.RFC3339),
req.Method,
req.URL.Path,
resp.StatusCode,
parentErr.Error(),
)
} else {
logEntry = fmt.Sprintf("Date: %s, Method: %s, Path: %s, Status: %d\n",
startTime.Format(time.RFC3339),
req.Method,
req.URL.Path,
resp.StatusCode,
)
}
// Open log file in append mode.
file, err := os.OpenFile(r.logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return resp, fmt.Errorf("failed to open log file: %w", err)
}
defer file.Close()
// Write log entry to file.
if _, err := file.WriteString(logEntry); err != nil {
return resp, fmt.Errorf("failed to write log entry to file: %w", err)
}
return resp, parentErr
}
type AnalyzerRoundTripper struct {
parent http.RoundTripper
}
func (r AnalyzerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := r.parent.RoundTrip(req)
if err != nil || IsMethodSafe(req.Method) {
return resp, err
}
// Check that unsafe methods did NOT return a valid status code.
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return resp, fmt.Errorf("non-safe request returned success")
}
return resp, nil
}
// IsMethodSafe is a helper method to check whether the HTTP method is safe according to MDN Web Docs.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods#safe_idempotent_and_cacheable_request_methods
func IsMethodSafe(method string) bool {
switch strings.ToUpper(method) {
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:
return true
default:
return false
}
}
type RateLimitRoundTripper struct {
parent http.RoundTripper
limiter *rate.Limiter
}
func (rt RateLimitRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if rt.parent == nil {
rt.parent = http.DefaultTransport
}
if rt.limiter != nil {
if err := rt.limiter.Wait(req.Context()); err != nil {
return nil, err
}
}
return rt.parent.RoundTrip(req)
}
func WithRateLimiter(l *rate.Limiter) ClientOption {
return func(c *http.Client) {
c.Transport = RateLimitRoundTripper{
parent: c.Transport,
limiter: l,
}
}
}
================================================
FILE: pkg/analyzer/analyzers/client_test.go
================================================
package analyzers
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestAnalyzerClientUnsafeSuccess(t *testing.T) {
testCases := []struct {
name string
method string
expectedStatus int
expectedError bool
}{
{
name: "Safe method (GET)",
method: http.MethodGet,
expectedStatus: http.StatusOK,
expectedError: false,
},
{
name: "Safe method (HEAD)",
method: http.MethodHead,
expectedStatus: http.StatusOK,
expectedError: false,
},
{
name: "Safe method (OPTIONS)",
method: http.MethodOptions,
expectedStatus: http.StatusOK,
expectedError: false,
},
{
name: "Safe method (TRACE)",
method: http.MethodTrace,
expectedStatus: http.StatusOK,
expectedError: false,
},
{
name: "Unsafe method (POST) with success status",
method: http.MethodPost,
expectedStatus: http.StatusOK,
expectedError: true,
},
{
name: "Unsafe method (PUT) with success status",
method: http.MethodPut,
expectedStatus: http.StatusOK,
expectedError: true,
},
{
name: "Unsafe method (DELETE) with success status",
method: http.MethodDelete,
expectedStatus: http.StatusOK,
expectedError: true,
},
{
name: "Unsafe method (POST) with error status",
method: http.MethodPost,
expectedStatus: http.StatusInternalServerError,
expectedError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a test server that returns the expected status code
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tc.expectedStatus)
}))
defer server.Close()
// Create a test request
req, err := http.NewRequest(tc.method, server.URL, nil)
if err != nil {
t.Fatalf("Failed to create test request: %v", err)
}
// Create the AnalyzerRoundTripper with a test client
client := NewAnalyzeClient(nil)
// Perform the request
resp, err := client.Do(req)
if resp != nil {
_ = resp.Body.Close()
}
// Check the error
if err != nil && !tc.expectedError {
t.Errorf("Unexpected error: %v", err)
} else if err == nil && tc.expectedError {
t.Errorf("Expected error, but got nil")
}
// Check the response status code
if resp != nil && resp.StatusCode != tc.expectedStatus {
t.Errorf("Expected status code: %d, but got: %d", tc.expectedStatus, resp.StatusCode)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/databricks/databricks.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go databricks
package databricks
import (
"fmt"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeDataBricks
}
func (a Analyzer) Analyze(ctx context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
token, exist := credInfo["token"]
if !exist {
return nil, fmt.Errorf("key not found in credential info")
}
domain, exist := credInfo["domain"]
if !exist {
return nil, fmt.Errorf("domain not found in credential info")
}
info, err := AnalyzePermissions(ctx, a.Cfg, domain, token)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, domain, token string) {
ctx := context.Background()
info, err := AnalyzePermissions(ctx, cfg, domain, token)
if err != nil {
// just print the error in cli and continue as a partial success
color.Red("[x] Error : %s", err.Error())
}
if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}
color.Green("[!] Valid DataBricks Access Token\n\n")
printUserInfo(info.UserInfo)
printTokenInfo(info.Tokens)
printPermissions(info.TokenPermissionLevels)
if len(info.Resources) > 0 {
printResources(info.Resources)
}
color.Yellow("\n[i] Expires: %s", "N/A (Refer to Token Information Table)")
}
func AnalyzePermissions(ctx context.Context, cfg *config.Config, domain, token string) (*SecretInfo, error) {
client := analyzers.NewAnalyzeClient(cfg)
var secretInfo = &SecretInfo{}
if err := captureUserInfo(ctx, client, domain, token, secretInfo); err != nil {
return nil, err
}
if err := captureTokensInfo(ctx, client, domain, token, secretInfo); err != nil {
return secretInfo, err
}
if err := captureTokenPermissions(ctx, client, domain, token, secretInfo); err != nil {
return secretInfo, err
}
// capture resources
if err := captureDataBricksResources(ctx, client, domain, token, secretInfo); err != nil {
return secretInfo, err
}
return secretInfo, nil
}
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeDataBricks,
Metadata: map[string]any{},
Bindings: make([]analyzers.Binding, 0),
}
// extract information from resource to create bindings and append to result bindings
for _, resource := range info.Resources {
binding := analyzers.Binding{
Resource: analyzers.Resource{
Name: resource.Name,
FullyQualifiedName: fmt.Sprintf("databricks/%s/%s", resource.Type, resource.ID), // e.g: netlify/site/123
Type: resource.Type,
Metadata: map[string]any{}, // to avoid panic
},
}
for key, value := range resource.Metadata {
binding.Resource.Metadata[key] = value
}
// for each permission add a binding to resource
for _, perm := range info.TokenPermissionLevels {
binding.Permission = analyzers.Permission{
Value: perm,
}
result.Bindings = append(result.Bindings, binding)
}
}
return &result
}
// cli print functions
func printUserInfo(user User) {
color.Yellow("[i] User Information:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"ID", "UserName", "Primary Email"})
t.AppendRow(table.Row{color.GreenString(user.ID), color.GreenString(user.UserName), color.GreenString(user.PrimaryEmail)})
t.Render()
}
func printTokenInfo(tokens []Token) {
color.Yellow("[i] Tokens Information:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Name", "Expiry Time", "Created By", "Last Used At"})
for _, token := range tokens {
t.AppendRow(table.Row{color.GreenString(token.Name),
color.GreenString(token.ExpiryTime), color.GreenString(token.CreatedBy), color.GreenString(token.LastUsedDay)})
}
t.Render()
}
func printPermissions(permissions []string) {
color.Yellow("[i] Token Permission Levels:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Permission Level"})
for _, permission := range permissions {
t.AppendRow(table.Row{color.GreenString(permission)})
}
t.Render()
}
func printResources(resources []DataBricksResource) {
color.Yellow("[i] Resources:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Name", "Type"})
for _, resource := range resources {
t.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/databricks/databricks_test.go
================================================
package databricks
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed result_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "analyzers1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
token := testSecrets.MustGetField("DATABRICKS_TOKEN")
domain := testSecrets.MustGetField("DATABRICKS_DOMAIN")
tests := []struct {
name string
domain string
token string
want []byte // JSON string
wantErr bool
}{
{
name: "valid databricks credentials",
domain: domain,
token: token,
want: expectedOutput,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"token": tt.token, "domain": tt.domain})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/databricks/models.go
================================================
package databricks
type ResourceType string
func (r ResourceType) String() string {
return string(r)
}
const (
CurrentUser ResourceType = "User"
TokensInfo ResourceType = "Token"
TokenPermissions ResourceType = "Token Permission"
Repositories ResourceType = "Repository"
GitCredentials ResourceType = "Git Credential"
Jobs ResourceType = "Job"
Clusters ResourceType = "Cluster"
Groups ResourceType = "Group"
Users ResourceType = "Member"
)
type SecretInfo struct {
UserInfo User
TokenPermissionLevels []string
Tokens []Token
Resources []DataBricksResource
}
type User struct {
ID string
UserName string
PrimaryEmail string
}
type Token struct {
ID string
Name string
ExpiryTime string
CreatedBy string
LastUsedDay string
}
type DataBricksResource struct {
ID string
Name string
Type string
Metadata map[string]string
}
// API response models
type CurrentUserInfo struct {
ID string `json:"id"`
UserName string `json:"userName"`
Emails []struct {
Display string `json:"display"`
Value string `json:"value"`
Primary bool `json:"primary"`
} `json:"emails"`
}
type Tokens struct {
TokensInfo []struct {
ID string `json:"token_id"`
Name string `json:"comment"`
ExpiryTime int `json:"expiry_time"`
LastUsedDay int `json:"last_used_day"`
CreatedBy string `json:"created_by_username"`
} `json:"token_infos"`
}
type Permissions struct {
PermissionLevels []struct {
Description string `json:"description"`
PermissionLevel string `json:"permission_level"`
} `json:"permission_levels"`
}
type ReposResponse struct {
Repositories []struct {
ID string `json:"id"`
Path string `json:"path"`
Provider string `json:"provider"`
URL string `json:"url"`
} `json:"repos"`
}
type GitCreds struct {
Credentials []struct {
ID string `json:"credentials_id"`
UserName string `json:"git_username"`
Provider string `json:"git_provider"`
} `json:"credentials"`
}
type JobsResponse struct {
Jobs []struct {
ID string `json:"job_id"`
Name string `json:"name"`
Description string `json:"description"`
} `json:"jobs"`
}
type ClustersResponse struct {
Clusters []struct {
ID string `json:"cluster_id"`
Name string `json:"cluster_name"`
CreatedBy string `json:"creator_user_name"`
} `json:"clusters"`
}
type GroupsResponse struct {
Resources []struct {
ID string `json:"id"`
Name string `json:"displayName"`
// TODO: capture members if needed
} `json:"Resources"`
}
type UsersResponse struct {
Resources []struct {
ID string `json:"id"`
UserName string `json:"userName"`
Active bool `json:"active"`
} `json:"Resources"`
}
================================================
FILE: pkg/analyzer/analyzers/databricks/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package databricks
import "errors"
type Permission int
const (
Invalid Permission = iota
CanManage Permission = iota
CanUse Permission = iota
)
var (
PermissionStrings = map[Permission]string{
CanManage: "CAN_MANAGE",
CanUse: "CAN_USE",
}
StringToPermission = map[string]Permission{
"CAN_MANAGE": CanManage,
"CAN_USE": CanUse,
}
PermissionIDs = map[Permission]int{
CanManage: 1,
CanUse: 2,
}
IdToPermission = map[int]Permission{
1: CanManage,
2: CanUse,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/databricks/permissions.yaml
================================================
permissions:
- CAN_MANAGE
- CAN_USE
================================================
FILE: pkg/analyzer/analyzers/databricks/requests.go
================================================
package databricks
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var (
// ErrUnauthorized is returned when the Databricks API answers with HTTP-401.
errUnAuthorized = errors.New("invalid/expired personal access token")
apiEndpoints = map[ResourceType]string{
CurrentUser: "/api/2.0/preview/scim/v2/Me",
TokensInfo: "/api/2.0/token-management/tokens",
TokenPermissions: "/api/2.0/permissions/authorization/tokens/permissionLevels",
Repositories: "/api/2.0/repos",
GitCredentials: "/api/2.0/git-credentials",
Jobs: "/api/2.2/jobs/list",
Clusters: "/api/2.1/clusters/list",
Groups: "/api/2.0/preview/scim/v2/Groups",
Users: "/api/2.0/preview/scim/v2/Users",
/*
TODO:
- https://docs.databricks.com/api/gcp/workspace/workspace/list (list content inside path)
- http://docs.databricks.com/api/gcp/workspace/libraries/allclusterlibrarystatuses (list cluster statuses)
*/
}
)
// doAndDecode performs an authenticated GET request against the constructed
// Databricks URL and JSON-decodes the response into the supplied result.
//
// The generic type parameter T allows the caller to decide which concrete
// struct the response should be unmarshalled into:
func doAndDecode[T any](ctx context.Context, client *http.Client, domain string, rt ResourceType, token string, out *T) error {
u := url.URL{
Scheme: "https",
Host: domain,
Path: apiEndpoints[rt],
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), http.NoBody)
if err != nil {
return fmt.Errorf("building request: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("Accept", "application/json")
// Execute request and read / decode body. We stream directly into the
// decoder instead of loading the whole response into memory first.
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("performing request: %w", err)
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
return fmt.Errorf("decoding response: %w", err)
}
return nil
case http.StatusUnauthorized:
return errUnAuthorized
default:
return fmt.Errorf("unexpected status code %d for API %s", resp.StatusCode, apiEndpoints[rt])
}
}
func captureDataBricksResources(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
if err := captureRepos(ctx, client, domain, token, secretInfo); err != nil {
return err
}
if err := captureGitCreds(ctx, client, domain, token, secretInfo); err != nil {
return err
}
if err := captureJobs(ctx, client, domain, token, secretInfo); err != nil {
return err
}
if err := captureClusters(ctx, client, domain, token, secretInfo); err != nil {
return err
}
if err := captureGroups(ctx, client, domain, token, secretInfo); err != nil {
return err
}
if err := captureUsers(ctx, client, domain, token, secretInfo); err != nil {
return err
}
return nil
}
func captureUserInfo(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var user CurrentUserInfo
if err := doAndDecode(ctx, client, domain, CurrentUser, token, &user); err != nil {
return err
}
secretInfo.UserInfo = User{
ID: user.ID,
UserName: user.UserName,
}
for _, email := range user.Emails {
if email.Primary {
secretInfo.UserInfo.PrimaryEmail = email.Value
}
}
return nil
}
func captureTokensInfo(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var tokens Tokens
if err := doAndDecode(ctx, client, domain, TokensInfo, token, &tokens); err != nil {
return err
}
for _, t := range tokens.TokensInfo {
secretInfo.Tokens = append(secretInfo.Tokens, Token{
ID: t.ID,
Name: t.Name,
ExpiryTime: readableTime(t.ExpiryTime),
LastUsedDay: readableTime(t.LastUsedDay),
CreatedBy: t.CreatedBy,
})
}
return nil
}
func captureTokenPermissions(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var permissions Permissions
if err := doAndDecode(ctx, client, domain, TokenPermissions, token, &permissions); err != nil {
return err
}
for _, item := range permissions.PermissionLevels {
secretInfo.TokenPermissionLevels = append(secretInfo.TokenPermissionLevels, item.PermissionLevel)
}
return nil
}
func captureRepos(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var repos ReposResponse
if err := doAndDecode(ctx, client, domain, Repositories, token, &repos); err != nil {
return err
}
for _, repo := range repos.Repositories {
if repo.ID == "" {
repo.ID = repo.URL
}
secretInfo.Resources = append(secretInfo.Resources, DataBricksResource{
ID: repo.ID,
Name: repo.Path,
Type: Repositories.String(),
Metadata: map[string]string{
"provider": repo.Provider,
"url": repo.URL,
},
})
}
return nil
}
func captureGitCreds(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var creds GitCreds
if err := doAndDecode(ctx, client, domain, GitCredentials, token, &creds); err != nil {
return err
}
for _, credential := range creds.Credentials {
secretInfo.Resources = append(secretInfo.Resources, DataBricksResource{
ID: credential.ID,
Name: credential.UserName,
Type: GitCredentials.String(),
Metadata: map[string]string{
"provider": credential.Provider,
},
})
}
return nil
}
func captureJobs(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var jobs JobsResponse
if err := doAndDecode(ctx, client, domain, Jobs, token, &jobs); err != nil {
return err
}
for _, job := range jobs.Jobs {
secretInfo.Resources = append(secretInfo.Resources, DataBricksResource{
ID: job.ID,
Name: job.Name,
Type: Jobs.String(),
Metadata: map[string]string{
"description": job.Description,
},
})
}
return nil
}
func captureClusters(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var clusters ClustersResponse
if err := doAndDecode(ctx, client, domain, Clusters, token, &clusters); err != nil {
return err
}
for _, cluster := range clusters.Clusters {
secretInfo.Resources = append(secretInfo.Resources, DataBricksResource{
ID: cluster.ID,
Name: cluster.Name,
Type: Clusters.String(),
Metadata: map[string]string{
"created by": cluster.CreatedBy,
},
})
}
return nil
}
func captureGroups(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var groups GroupsResponse
if err := doAndDecode(ctx, client, domain, Groups, token, &groups); err != nil {
return err
}
for _, group := range groups.Resources {
secretInfo.Resources = append(secretInfo.Resources, DataBricksResource{
ID: group.ID,
Name: group.Name,
Type: Groups.String(),
})
}
return nil
}
func captureUsers(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var users UsersResponse
if err := doAndDecode(ctx, client, domain, Users, token, &users); err != nil {
return err
}
for _, user := range users.Resources {
secretInfo.Resources = append(secretInfo.Resources, DataBricksResource{
ID: user.ID,
Name: user.UserName,
Type: Users.String(),
Metadata: map[string]string{
"active": fmt.Sprintf("%t", user.Active),
},
})
}
return nil
}
func readableTime(timestamp int) string {
timestampMillis := int64(timestamp)
t := time.Unix(timestampMillis/1000, (timestampMillis%1000)*int64(time.Millisecond))
return t.Format("2006-01-02 15:04:05")
}
================================================
FILE: pkg/analyzer/analyzers/databricks/result_output.json
================================================
{
"AnalyzerType": 41,
"Bindings": [
{
"Resource": {
"Name": "admins",
"FullyQualifiedName": "databricks/Group/601448505198850",
"Type": "Group",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "CAN_MANAGE",
"Parent": null
}
},
{
"Resource": {
"Name": "admins",
"FullyQualifiedName": "databricks/Group/601448505198850",
"Type": "Group",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "CAN_USE",
"Parent": null
}
},
{
"Resource": {
"Name": "kashif.khan@trufflesec.com",
"FullyQualifiedName": "databricks/Member/8639341364955455",
"Type": "Member",
"Metadata": {
"active": "true"
},
"Parent": null
},
"Permission": {
"Value": "CAN_MANAGE",
"Parent": null
}
},
{
"Resource": {
"Name": "kashif.khan@trufflesec.com",
"FullyQualifiedName": "databricks/Member/8639341364955455",
"Type": "Member",
"Metadata": {
"active": "true"
},
"Parent": null
},
"Permission": {
"Value": "CAN_USE",
"Parent": null
}
},
{
"Resource": {
"Name": "kashifkhan",
"FullyQualifiedName": "databricks/Git Credential/",
"Type": "Git Credential",
"Metadata": {
"provider": "gitHub"
},
"Parent": null
},
"Permission": {
"Value": "CAN_MANAGE",
"Parent": null
}
},
{
"Resource": {
"Name": "kashifkhan",
"FullyQualifiedName": "databricks/Git Credential/",
"Type": "Git Credential",
"Metadata": {
"provider": "gitHub"
},
"Parent": null
},
"Permission": {
"Value": "CAN_USE",
"Parent": null
}
},
{
"Resource": {
"Name": "users",
"FullyQualifiedName": "databricks/Group/1000729253926373",
"Type": "Group",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "CAN_MANAGE",
"Parent": null
}
},
{
"Resource": {
"Name": "users",
"FullyQualifiedName": "databricks/Group/1000729253926373",
"Type": "Group",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "CAN_USE",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": {}
}
================================================
FILE: pkg/analyzer/analyzers/datadog/datadog.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go datadog
package datadog
import (
"errors"
"fmt"
"net/url"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeDatadog
}
// Analyze performs the analysis of the Datadog API key and returns the analyzer result.
func (a Analyzer) Analyze(ctx context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
apiKey := credInfo["api_key"]
appKey := credInfo["app_key"]
endpoint := credInfo["endpoint"]
info, err := AnalyzePermissions(a.Cfg, apiKey, appKey, endpoint)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, apiKey, appKey, endpoint string) {
info, err := AnalyzePermissions(cfg, apiKey, appKey, endpoint)
if err != nil {
// just print the error in cli and continue as a partial success
color.Red("[x] Error : %s", err.Error())
}
if info == nil {
color.Red("[x] No information retrieved")
return
}
color.Green("[i] Valid Datadog API Key\n")
printUser(info.User)
printResources(info.Resources)
printPermissions(info.Permissions)
}
// AnalyzePermissions will collect all the scopes assigned to token along with resource it can access
func AnalyzePermissions(cfg *config.Config, apiKey, appKey, endpoint string) (*SecretInfo, error) {
if apiKey == "" {
return nil, errors.New("api key not found in credentials info")
}
// create the http client
client := analyzers.NewAnalyzeClient(cfg)
var secretInfo = &SecretInfo{}
var baseURL string
var err error
// If endpoint is provided, use it directly; otherwise detect domain
if endpoint != "" {
baseURL, err = url.JoinPath(endpoint, "api")
if err != nil {
return nil, fmt.Errorf("failed to join path: %w", err)
}
} else {
baseURL, err = DetectDomain(client, apiKey, appKey)
if err != nil {
return nil, fmt.Errorf("[x] %v", err)
}
}
if appKey == "" {
// If no application key is provided, we can only validate the API key
if endpoint != "" {
// If endpoint is empty we don't need to validate again because DetectDomain would have already validated the API key against detected domain
// But if endpoint is provided, we should validate the API key against the provided endpoint to ensure it's valid before proceeding
isValidApiKey, err := ValidateApiKey(client, baseURL, apiKey)
if err != nil {
return nil, fmt.Errorf("failed to validate api key: %v", err)
}
if !isValidApiKey {
return nil, errors.New("invalid api key provided")
}
}
if err := CaptureApiKeyPermissions(secretInfo); err != nil {
return nil, fmt.Errorf("failed to fetch permissions: %v", err)
}
return secretInfo, nil
}
// capture user information in secretInfo
// If the application key is scoped, user information cannot be retrieved even if all the permissions are granted
// This is a non-documented Endpoint and can lead to unexpected behavior in future updates
// If user information is not retrieved, we will move ahead with the rest of the analysis and print the error
_ = CaptureUserInformation(client, baseURL, apiKey, appKey, secretInfo)
// capture resources in secretInfo
if err := CaptureResources(client, baseURL, apiKey, appKey, secretInfo); err != nil {
return nil, fmt.Errorf("failed to fetch resources: %v", err)
}
// capture permissions in secretInfo
if err := CapturePermissions(client, baseURL, apiKey, appKey, secretInfo); err != nil {
return nil, fmt.Errorf("failed to fetch permissions: %v", err)
}
// Capture API key permissions
if err := CaptureApiKeyPermissions(secretInfo); err != nil {
return nil, fmt.Errorf("failed to fetch permissions: %v", err)
}
return secretInfo, nil
}
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeDatadog,
Metadata: map[string]any{},
Bindings: make([]analyzers.Binding, 0),
}
// Create user resource to use as parent
var userResource *analyzers.Resource
if info.User.Id != "" {
userResource = &analyzers.Resource{
FullyQualifiedName: info.User.Id,
Name: info.User.Name,
Type: "User",
Metadata: map[string]any{
"email": info.User.Email,
},
}
}
permissionBindings := secretInfoPermissionsToAnalyzerPermission(info.Permissions)
if userResource != nil && len(*permissionBindings) > 0 {
result.Bindings = analyzers.BindAllPermissions(*userResource, *permissionBindings...)
}
if userResource == nil && len(*permissionBindings) > 0 {
result.Bindings = analyzers.BindAllPermissions(analyzers.Resource{
FullyQualifiedName: "Unknown User",
Name: "Unknown User",
Type: "User",
Metadata: map[string]any{},
}, *permissionBindings...)
}
// Extract information from resources to create bindings
for _, resource := range info.Resources {
resource := secretInfoResourceToAnalyzerResource(resource)
// Set the user resource as parent if available
if userResource != nil {
resource.Parent = userResource
}
binding := analyzers.Binding{
Resource: *resource,
}
result.Bindings = append(result.Bindings, binding)
}
return &result
}
// secretInfoPermissionsToAnalyzerPermission translate secret info Permission to analyzer resource for binding
func secretInfoPermissionsToAnalyzerPermission(perms []Permission) *[]analyzers.Permission {
permissions := make([]analyzers.Permission, 0, len(perms))
for _, perm := range perms {
permissions = append(permissions, analyzers.Permission{
Value: perm.Title,
})
}
return &permissions
}
// secretInfoResourceToAnalyzerResource translate secret info Resource to analyzer resource for binding
func secretInfoResourceToAnalyzerResource(resource Resource) *analyzers.Resource {
analyzerRes := analyzers.Resource{
FullyQualifiedName: resource.ID,
Name: resource.Name,
Type: resource.Type,
Metadata: map[string]any{},
}
for key, value := range resource.MetaData {
analyzerRes.Metadata[key] = value
}
return &analyzerRes
}
func printUser(user User) {
if user.Id == "" {
color.Red("\n[x] User information not available")
return
}
color.Green("\n[i] User Information:")
userTable := table.NewWriter()
userTable.SetOutputMirror(os.Stdout)
userTable.AppendHeader(table.Row{"User Id", "Name", "Email"})
userTable.AppendRow(table.Row{color.GreenString(user.Id), color.GreenString(user.Name), color.GreenString(user.Email)})
userTable.Render()
}
func printResources(resources []Resource) {
if len(resources) == 0 {
color.Red("[x] No resources found")
return
}
color.Green("\n[i] Resources:")
resourceTable := table.NewWriter()
resourceTable.SetOutputMirror(os.Stdout)
resourceTable.AppendHeader(table.Row{"Name", "Type"})
for _, resource := range resources {
resourceTable.AppendRow(table.Row{
color.GreenString(resource.Name),
color.GreenString(resource.Type),
})
}
resourceTable.Render()
}
func printPermissions(permissions []Permission) {
if len(permissions) == 0 {
color.Red("[x] No permissions found")
return
}
color.Green("\n[i] Permissions:")
permissionTable := table.NewWriter()
permissionTable.SetOutputMirror(os.Stdout)
permissionTable.AppendHeader(table.Row{"Title", "Name", "Description"})
// Set wrapping for long descriptions
permissionTable.SetColumnConfigs([]table.ColumnConfig{
{Number: 3, WidthMax: 50},
})
for _, permission := range permissions {
permissionTable.AppendRow(table.Row{
color.GreenString(permission.Title),
color.GreenString(permission.Name),
color.GreenString(permission.Description),
})
}
permissionTable.Render()
}
================================================
FILE: pkg/analyzer/analyzers/datadog/datadog_test.go
================================================
package datadog
import (
_ "embed"
"encoding/json"
"fmt"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
//go:embed expected_output_apikey.json
var expectedOutputAPIKey []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*2)
defer cancel()
// Get API keys from GCP
var apiKey, appKey string
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "analyzers1")
if err != nil {
t.Fatalf("Could not get test secrets from GCP: %s", err)
}
// Get the required credentials
apiKey = testSecrets.MustGetField("DATADOG_API_KEY")
appKey = testSecrets.MustGetField("DATADOG_APP_KEY")
// Fail if credentials are not available
if apiKey == "" || appKey == "" {
t.Fatalf("Datadog credentials are required for this test")
}
tests := []struct {
name string
apiKey string
appKey string
endpoint string
want []byte // JSON string
wantErr bool
}{
{
name: "valid datadog credentials",
apiKey: apiKey,
appKey: appKey,
want: expectedOutput,
wantErr: false,
},
{
name: "valid datadog credentials with endpoint",
apiKey: apiKey,
appKey: appKey,
endpoint: "https://api.us5.datadoghq.com",
want: expectedOutput,
wantErr: false,
},
{
name: "valid datadog credentials with invalid endpoint",
apiKey: apiKey,
appKey: appKey,
endpoint: "https://api.eu.datadoghq.com",
want: []byte(fmt.Sprintf(`{
"AnalyzerType": %d,
"Bindings": [],
"UnboundedResources": null,
"Metadata": {}
}`, analyzers.AnalyzerTypeDatadog)),
wantErr: true,
},
{
name: "invalid credentials",
apiKey: "invalid_api_key",
appKey: "invalid_app_key",
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"api_key": tt.apiKey, "app_key": tt.appKey, "endpoint": tt.endpoint})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Skip verification for error cases
if tt.wantErr {
return
}
// For valid cases, verify we got a result
if got == nil {
t.Errorf("Analyzer.Analyze() = nil, want non-nil")
return
}
// Verify type is correct
if got.AnalyzerType != analyzers.AnalyzerTypeDatadog {
t.Errorf("Analyzer.Analyze() returned wrong analyzer type, got %d want %d",
got.AnalyzerType, analyzers.AnalyzerTypeDatadog)
}
// Bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal(tt.want, &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
func TestAnalyzer_Analyze_ApiKeyOnly(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*2)
defer cancel()
// Get API keys from GCP
var apiKey string
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "analyzers1")
if err != nil {
t.Fatalf("Could not get test secrets from GCP: %s", err)
}
// Get the required credentials
apiKey = testSecrets.MustGetField("DATADOG_API_KEY")
// Fail if credentials are not available
if apiKey == "" {
t.Fatalf("Datadog credentials are required for this test")
}
want := expectedOutputAPIKey
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"api_key": apiKey, "endpoint": "https://api.us5.datadoghq.com"})
if err != nil {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, false)
return
}
// For valid cases, verify we got a result
if got == nil {
t.Errorf("Analyzer.Analyze() = nil, want non-nil")
return
}
// Verify type is correct
if got.AnalyzerType != analyzers.AnalyzerTypeDatadog {
t.Errorf("Analyzer.Analyze() returned wrong analyzer type, got %d want %d",
got.AnalyzerType, analyzers.AnalyzerTypeDatadog)
}
// Bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal(want, &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/datadog/expected_output.json
================================================
{
"AnalyzerType": 37,
"Bindings": [
{
"Resource": {
"Name": "My Monitor",
"FullyQualifiedName": "4429851",
"Type": "Monitor",
"Metadata": {},
"Parent": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
}
},
"Permission": {
"Value": "",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "API Keys Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "API Keys Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "APM Generate Metrics",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "APM Pipelines Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "APM Pipelines Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "APM Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "APM Retention Filters Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "APM Retention Filters Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "AWS Configurations Manage",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Audit Trail Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Azure Configurations Manage",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Connections Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Connections Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Dashboards Public Share",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Dashboards Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Dashboards Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Data Scanner Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Data Scanner Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "GCP Configurations Manage",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Incident Settings Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Incidents Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Integrations Manage",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Logs Generate Metrics",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Logs Modify Indexes",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Logs Read Archives",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Logs Read Data",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Logs Write Archives",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Logs Write Pipelines",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Manage Downtimes",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Metric Tags Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Monitor Configuration Policy Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Monitors Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Monitors Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Notebooks Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Notebooks Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Observability Pipelines Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Org App Keys Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Org App Keys Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Org Management",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "RUM Apps Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "RUM Apps Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "SLOs Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "SLOs Status Corrections",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "SLOs Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Security Filters Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Security Filters Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Security Rules Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Security Signals Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Security Signals Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Service Account Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Submit Events",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Submit Logs",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Submit Metrics",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Submit Service Checks",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Synthetics Default Settings Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Synthetics Global Variable Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Synthetics Global Variable Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Synthetics Private Locations Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Synthetics Private Locations Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Synthetics Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Synthetics Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Usage Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "User Access Invite",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "User Access Manage",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "User App Keys",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Workflows Read",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
},
"Permission": {
"Value": "Workflows Write",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Truffle's Dashboard",
"FullyQualifiedName": "fvx-idw-ani",
"Type": "Dashboard",
"Metadata": {
"Author Handle": "detectors@trufflesec.com",
"Layout Type": "ordered",
"URL": "/dashboard/fvx-idw-ani/truffles-dashboard"
},
"Parent": {
"Name": "Truffle Sec",
"FullyQualifiedName": "a4b3b545-24ec-11f0-9f57-22795724d251",
"Type": "User",
"Metadata": {
"email": "detectors@trufflesec.com"
},
"Parent": null
}
},
"Permission": {
"Value": "",
"Parent": null
},
"Condition": ""
}
],
"UnboundedResources": null,
"Metadata": {}
}
================================================
FILE: pkg/analyzer/analyzers/datadog/expected_output_apikey.json
================================================
{
"AnalyzerType": 37,
"Bindings": [
{
"Resource": {
"Name": "Unknown User",
"FullyQualifiedName": "Unknown User",
"Type": "User",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "Submit Events",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Unknown User",
"FullyQualifiedName": "Unknown User",
"Type": "User",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "Submit Logs",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Unknown User",
"FullyQualifiedName": "Unknown User",
"Type": "User",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "Submit Metrics",
"Parent": null
},
"Condition": ""
},
{
"Resource": {
"Name": "Unknown User",
"FullyQualifiedName": "Unknown User",
"Type": "User",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "Submit Service Checks",
"Parent": null
},
"Condition": ""
}
],
"UnboundedResources": null,
"Metadata": {}
}
================================================
FILE: pkg/analyzer/analyzers/datadog/models.go
================================================
package datadog
import "sync"
// Resource type constants for consistent usage
const (
ResourceTypeValidate = "Validate"
ResourceTypeCurrentUser = "Current User"
ResourceTypeDashboard = "Dashboard"
ResourceTypeMonitor = "Monitor"
)
// Permission represents a permission granted to an API key
type Permission struct {
Name string
Title string
Description string
MetaData map[string]string
}
// SecretInfo holds all information gathered about a Datadog API key
type SecretInfo struct {
User User
Permissions []Permission
mu sync.RWMutex
Resources []Resource
}
// User is the information about the user to whom the token belongs
type User struct {
Id string
Name string
Email string
}
// Resource represents a Datadog resource
type Resource struct {
ID string
Name string
Type string
MetaData map[string]string
}
// API response structures
type currentUserResponse struct {
Data struct {
Id string `json:"id"`
Attributes struct {
Name string `json:"name"`
Email string `json:"email"`
} `json:"attributes"`
} `json:"data"`
}
type dashboardResponse struct {
Dashboards []DashboardItem `json:"dashboards"`
}
type DashboardItem struct {
ID string `json:"id"`
Title string `json:"title"`
URL string `json:"url"`
IsReadOnly bool `json:"is_read_only"`
CreatedAt string `json:"created_at"`
ModifiedAt string `json:"modified_at"`
AuthorHandle string `json:"author_handle"`
Description *string `json:"description"`
LayoutType string `json:"layout_type"`
DeletedAt *string `json:"deleted_at"`
}
type monitorResponse []struct {
ID int `json:"id"`
Name string `json:"name"`
}
// appendResource adds a resource to secret info resources list
func (s *SecretInfo) appendResource(resource Resource) {
s.mu.Lock()
defer s.mu.Unlock()
s.Resources = append(s.Resources, resource)
}
================================================
FILE: pkg/analyzer/analyzers/datadog/permissions.yaml
================================================
permissions:
- dashboards_read
- dashboards_write
- dashboards_public_share
- monitors_read
- monitors_write
- logs_modify_indexes
- logs_write_pipelines
- logs_write_archives
- logs_generate_metrics
- monitors_downtime
- logs_read_data
- logs_read_archives
- security_monitoring_rules_read
- security_monitoring_rules_write
- security_monitoring_signals_read
- security_monitoring_signals_write
- user_access_invite
- user_app_keys
- org_app_keys_read
- org_app_keys_write
- user_access_manage
- synthetics_private_location_read
- synthetics_private_location_write
- usage_read
- metric_tags_write
- audit_logs_read
- api_keys_read
- api_keys_write
- synthetics_global_variable_read
- synthetics_global_variable_write
- synthetics_read
- synthetics_write
- synthetics_default_settings_read
- service_account_write
- apm_read
- apm_retention_filter_read
- apm_retention_filter_write
- rum_apps_write
- data_scanner_read
- data_scanner_write
- org_management
- security_monitoring_filters_read
- security_monitoring_filters_write
- incident_read
- incident_write
- incident_settings_write
- rum_apps_read
- security_monitoring_notification_profiles_read
- security_monitoring_notification_profiles_write
- apm_generate_metrics
- apm_pipelines_write
- apm_pipelines_read
- observability_pipelines_read
- workflows_read
- workflows_write
- workflows_run
- connections_read
- connections_write
- notebooks_read
- notebooks_write
- aws_configurations_manage
- azure_configurations_manage
- gcp_configurations_manage
- manage_integrations
- slos_read
- slos_write
- slos_corrections
- monitor_config_policy_write
================================================
FILE: pkg/analyzer/analyzers/datadog/requests.go
================================================
package datadog
import (
"context"
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"slices"
"strconv"
"sync"
"time"
)
// Constants and configuration
const (
defaultTimeout = 12 * time.Second
apiKeyHeader = "DD-API-KEY"
appKeyHeader = "DD-APPLICATION-KEY"
)
// List of all DataDog domains to try
var datadogDomains = []string{
"https://api.us5.datadoghq.com/api", // Default domain
"https://api.app.datadoghq.com/api",
"https://api.us3.datadoghq.com/api",
"https://api.app.datadoghq.eu/api",
"https://api.app.ddog-gov.com/api",
"https://api.ap1.datadoghq.com/api",
}
// Endpoints map for API paths
var endpoints = map[string]string{
ResourceTypeCurrentUser: "/v2/current_user",
ResourceTypeDashboard: "/v1/dashboard",
ResourceTypeMonitor: "/v1/monitor",
ResourceTypeValidate: "/v1/validate",
}
//go:embed scopes.json
var scopesConfig []byte
// --------------------------------
// Data models
// --------------------------------
// HttpStatusTest defines a test for checking HTTP endpoint permissions
type HttpStatusTest struct {
Method string `json:"method"`
Endpoint string `json:"endpoint"`
ValidStatuses []int `json:"valid_statuses"`
InvalidStatuses []int `json:"invalid_statuses"`
}
// Scope represents a permission scope with a test
type Scope struct {
Name string `json:"name"`
Title string `json:"title"`
Description string `json:"description"`
Resource string `json:"resource"`
HttpTest HttpStatusTest `json:"test"`
}
// --------------------------------
// Domain detection
// --------------------------------
// DetectDomain tries each DataDog domain to find a working one
func DetectDomain(client *http.Client, apiKey string, appKey string) (string, error) {
for _, domain := range datadogDomains {
// Use a simple endpoint to test if the domain works
endpoint := domain + endpoints[ResourceTypeValidate]
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
// Create request
req, err := http.NewRequestWithContext(ctx, "GET", endpoint, http.NoBody)
if err != nil {
continue // Skip to next domain if request creation fails
}
// Add required keys in the header
req.Header.Set(apiKeyHeader, apiKey)
if appKey != "" {
req.Header.Set(appKeyHeader, appKey)
}
resp, err := client.Do(req)
if err != nil {
continue // Skip to next domain if request fails
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
// If we get a response that's not a connection error, this domain works
if resp.StatusCode == http.StatusOK {
return domain, nil
}
}
return "", errors.New("unable to validate any DataDog domain with the provided API key")
}
// --------------------------------
// HTTP request utilities
// --------------------------------
// makeDataDogRequest sends an HTTP GET API request to the specified endpoint with auth tokens
func makeDataDogRequest(client *http.Client, baseURL, endpoint, method, apiKey string, appKey string) ([]byte, int, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
// create request
url, err := url.JoinPath(baseURL, endpoint)
if err != nil {
return nil, 0, fmt.Errorf("failed to build URL: %w", err)
}
req, err := http.NewRequestWithContext(ctx, method, url, http.NoBody)
if err != nil {
return nil, 0, err
}
// add required keys in the header
req.Header.Set(apiKeyHeader, apiKey)
if appKey != "" {
req.Header.Set(appKeyHeader, appKey)
}
resp, err := client.Do(req)
if err != nil {
return nil, 0, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
responseBodyByte, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, err
}
return responseBodyByte, resp.StatusCode, nil
}
// RunTest executes an HTTP test against an API endpoint with provided headers
func (h *HttpStatusTest) RunTest(client *http.Client, baseURL string, headers map[string]string) (bool, error) {
apiKey := headers[apiKeyHeader]
appKey := headers[appKeyHeader]
_, statusCode, err := makeDataDogRequest(client, baseURL, h.Endpoint, h.Method, apiKey, appKey)
if err != nil {
fmt.Printf("Error making request: %v\n", err)
return false, err
}
// Check response status code
switch {
case slices.Contains(h.ValidStatuses, statusCode):
return true, nil
case slices.Contains(h.InvalidStatuses, statusCode):
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// --------------------------------
// Validate ApiKey
// --------------------------------
func ValidateApiKey(client *http.Client, baseURL, apiKey string) (bool, error) {
// Use a simple endpoint to test if the domain works
endpoint, err := url.JoinPath(baseURL, endpoints[ResourceTypeValidate])
if err != nil {
return false, fmt.Errorf("failed to build endpoint: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
// Create request
req, err := http.NewRequestWithContext(ctx, "GET", endpoint, http.NoBody)
if err != nil {
return false, err
}
// Add required keys in the header
req.Header.Set(apiKeyHeader, apiKey)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
// If we get a response that's not a connection error, this domain works
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("Unable to validate api key with status code: %d", resp.StatusCode)
}
}
// --------------------------------
// Data capture functions
// --------------------------------
// CaptureUserInformation retrieves and stores user information
func CaptureUserInformation(client *http.Client, baseURL, apiKey, appKey string, secretInfo *SecretInfo) error {
caller, err := getCurrentUserInfo(client, baseURL, apiKey, appKey)
if err != nil {
return err
}
addUserToSecretInfo(caller, secretInfo)
return nil
}
// CaptureResources retrieves and stores dashboard and monitor resources
func CaptureResources(client *http.Client, baseURL, apiKey, appKey string, secretInfo *SecretInfo) error {
var wg sync.WaitGroup
errChan := make(chan error, 2) // Buffer size matches the number of tasks
// helper to launch tasks concurrently
launchTask := func(task func() error) {
wg.Add(1)
go func() {
defer wg.Done()
if err := task(); err != nil {
errChan <- err
}
}()
}
launchTask(func() error { return captureDashboard(client, baseURL, apiKey, appKey, secretInfo) })
launchTask(func() error { return captureMonitor(client, baseURL, apiKey, appKey, secretInfo) })
// Wait for all tasks to complete
wg.Wait()
close(errChan)
// Collect any errors
var errs []error
for err := range errChan {
errs = append(errs, err)
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
// CapturePermissions tests and records available permissions
func CapturePermissions(client *http.Client, baseURL, apiKey, appKey string, secretInfo *SecretInfo) error {
scopes, err := readInScopes()
if err != nil {
return fmt.Errorf("reading in scopes: %w", err)
}
permissions := make([]Permission, 0)
headers := map[string]string{
apiKeyHeader: apiKey,
appKeyHeader: appKey,
}
for _, scope := range scopes {
if scope.HttpTest.Endpoint != "" {
status, err := scope.HttpTest.RunTest(client, baseURL, headers)
if err != nil {
return fmt.Errorf("running test for scope %s: %w", scope.Name, err)
}
metadata := map[string]string{
"Resource": scope.Resource,
}
if status {
permission := Permission{
Name: scope.Name,
Title: scope.Title,
Description: scope.Description,
MetaData: metadata,
}
permissions = append(permissions, permission)
}
}
}
secretInfo.Permissions = permissions
return nil
}
// API key is not finely grained, so we assign some default permissions
func CaptureApiKeyPermissions(secretInfo *SecretInfo) error {
scopes, err := readInScopes()
if err != nil {
return fmt.Errorf("reading in scopes: %w", err)
}
permissions := make([]Permission, 0)
for _, scope := range scopes {
metadata := map[string]string{
"Resource": scope.Resource,
}
if scope.HttpTest.Endpoint == "" {
permission := Permission{
Name: scope.Name,
Title: scope.Title,
Description: scope.Description,
MetaData: metadata,
}
permissions = append(permissions, permission)
}
}
secretInfo.Permissions = append(secretInfo.Permissions, permissions...)
return nil
}
// --------------------------------
// Resource capture helper functions
// --------------------------------
// getCurrentUserInfo retrieves information about the current user
func getCurrentUserInfo(client *http.Client, baseURL, apiKey, appKey string) (*currentUserResponse, error) {
response, statusCode, err := makeDataDogRequest(client, baseURL, endpoints[ResourceTypeCurrentUser], http.MethodGet, apiKey, appKey)
if err != nil {
return nil, err
}
switch statusCode {
case http.StatusOK:
var caller = ¤tUserResponse{}
if err := json.Unmarshal(response, caller); err != nil {
return nil, fmt.Errorf("unmarshalling user response: %w", err)
}
return caller, nil
case http.StatusUnauthorized:
return nil, errors.New("invalid API key or application key")
default:
return nil, fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// addUserToSecretInfo adds user information to the secret info object
func addUserToSecretInfo(caller *currentUserResponse, secretInfo *SecretInfo) {
user := User{
Id: caller.Data.Id,
Name: caller.Data.Attributes.Name,
Email: caller.Data.Attributes.Email,
}
secretInfo.User = user
}
// captureDashboard retrieves dashboard information
func captureDashboard(client *http.Client, baseURL, apiKey, appKey string, secretInfo *SecretInfo) error {
response, statusCode, err := makeDataDogRequest(client, baseURL, endpoints[ResourceTypeDashboard], http.MethodGet, apiKey, appKey)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var dashboardResponse = &dashboardResponse{}
if err := json.Unmarshal(response, dashboardResponse); err != nil {
return fmt.Errorf("unmarshalling dashboard response: %w", err)
}
for _, dashboard := range dashboardResponse.Dashboards {
metadata := map[string]string{
"Layout Type": dashboard.LayoutType,
"URL": dashboard.URL,
"Author Handle": dashboard.AuthorHandle,
}
resource := Resource{
ID: dashboard.ID,
Name: dashboard.Title,
Type: ResourceTypeDashboard,
MetaData: metadata,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code for dashboard API: %d", statusCode)
}
}
// captureMonitor retrieves monitor information
func captureMonitor(client *http.Client, baseURL, apiKey, appKey string, secretInfo *SecretInfo) error {
response, statusCode, err := makeDataDogRequest(client, baseURL, endpoints[ResourceTypeMonitor], http.MethodGet, apiKey, appKey)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var monitorResponse = &monitorResponse{}
if err := json.Unmarshal(response, monitorResponse); err != nil {
return fmt.Errorf("unmarshalling monitor response: %w", err)
}
for _, monitor := range *monitorResponse {
resource := Resource{
ID: strconv.Itoa(monitor.ID),
Name: monitor.Name,
Type: ResourceTypeMonitor,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code for monitor API: %d", statusCode)
}
}
// --------------------------------
// Utility functions
// --------------------------------
// readInScopes loads permission scopes from the embedded configuration
func readInScopes() ([]Scope, error) {
var scopes []Scope
if err := json.Unmarshal(scopesConfig, &scopes); err != nil {
return nil, fmt.Errorf("unmarshalling scopes config: %w", err)
}
return scopes, nil
}
================================================
FILE: pkg/analyzer/analyzers/datadog/scopes.json
================================================
[
{
"name": "dashboards_read",
"title": "Dashboards Read",
"description": "View dashboards.",
"resource": "Dashboards",
"test": {
"endpoint": "/v1/dashboard",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "dashboards_write",
"title": "Dashboards Write",
"description": "Create and change dashboards.",
"resource": "Dashboards",
"test": {
"endpoint": "/v1/dashboard",
"method": "POST",
"valid_statuses": [200, 400, 429],
"invalid_statuses": [403]
}
},
{
"name": "dashboards_public_share",
"title": "Dashboards Public Share",
"description": "Create, modify and delete shared dashboards with share type 'Public'. These dashboards can be accessed by anyone on the internet.",
"resource": "Dashboards",
"test": {
"endpoint": "/v1/dashboard/public",
"method": "POST",
"valid_statuses": [200, 400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "monitors_read",
"title": "Monitors Read",
"description": "View monitors.",
"resource": "Monitors",
"test": {
"endpoint": "/v1/monitor",
"method": "GET",
"valid_statuses": [200, 400, 429],
"invalid_statuses": [403]
}
},
{
"name": "monitors_write",
"title": "Monitors Write",
"description": "Edit and delete individual monitors.",
"resource": "Monitors",
"test": {
"endpoint": "/v1/monitor",
"method": "POST",
"valid_statuses": [200, 400, 429],
"invalid_statuses": [403]
}
},
{
"name": "logs_modify_indexes",
"title": "Logs Modify Indexes",
"description": "Read and modify all indexes in your account.",
"resource": "Logs",
"test": {
"endpoint": "/v1/logs/config/indexes/does-not-exist",
"method": "DELETE",
"valid_statuses": [200, 400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "logs_write_pipelines",
"title": "Logs Write Pipelines",
"description": "Add and change log pipeline configurations.",
"resource": "Logs",
"test": {
"endpoint": "/v1/logs/config/pipelines/does-not-exist",
"method": "DELETE",
"valid_statuses": [200, 400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "logs_write_archives",
"title": "Logs Write Archives",
"description": "Add and edit Log Archives.",
"resource": "Logs",
"test": {
"endpoint": "/v2/logs/config/archives/does-not-exist",
"method": "DELETE",
"valid_statuses": [200, 400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "logs_generate_metrics",
"title": "Logs Generate Metrics",
"description": "Create custom metrics from logs.",
"resource": "Logs",
"test": {
"endpoint": "/v2/logs/config/metrics",
"method": "POST",
"valid_statuses": [200, 400, 429],
"invalid_statuses": [403]
}
},
{
"name": "monitors_downtime",
"title": "Manage Downtimes",
"description": "Set downtimes to suppress alerts from any monitor in an organization.",
"resource": "Monitors",
"test": {
"endpoint": "/v1/downtime",
"method": "POST",
"valid_statuses": [200, 400, 429],
"invalid_statuses": [403]
}
},
{
"name": "logs_read_data",
"title": "Logs Read Data",
"description": "Read log data. In order to read log data, a user must have both this permission and Logs Read Index Data.",
"resource": "Logs",
"test": {
"endpoint": "/v2/logs/events",
"method": "GET",
"valid_statuses": [200, 400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "logs_read_archives",
"title": "Logs Read Archives",
"description": "Read Log Archives location and use it for rehydration.",
"resource": "Logs",
"test": {
"endpoint": "/v2/logs/config/archives",
"method": "GET",
"valid_statuses": [200, 400, 429],
"invalid_statuses": [403]
}
},
{
"name": "security_monitoring_rules_read",
"title": "Security Rules Read",
"description": "Read Detection Rules.",
"resource": "Security Monitoring",
"test": {
"endpoint": "/v2/cloud_security_management/custom_frameworks/must/not-exist",
"method": "GET",
"valid_statuses": [200, 400, 429],
"invalid_statuses": [403]
}
},
{
"name": "security_monitoring_rules_write",
"title": "Security Rules Write",
"description": "Create and edit Detection Rules.",
"resource": "Security Monitoring",
"test": {
"endpoint": "/v2/security_monitoring/rules",
"method": "POST",
"valid_statuses": [200, 400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "security_monitoring_signals_read",
"title": "Security Signals Read",
"description": "View Security Signals.",
"resource": "Security Monitoring",
"test": {
"endpoint": "/v2/security_monitoring/signals",
"method": "GET",
"valid_statuses": [200, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "security_monitoring_signals_write",
"title": "Security Signals Write",
"description": "Modify Security Signals.",
"resource": "Security Monitoring",
"test": {
"endpoint": "/v1/security_analytics/signals/must-not-exist/add_to_incident",
"method": "PATCH",
"valid_statuses": [200, 400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "user_access_invite",
"title": "User Access Invite",
"description": "Invite other users to your organization.",
"resource": "Users",
"test": {
"endpoint": "/v2/user_invitations/does-not-exist",
"method": "GET",
"valid_statuses": [200, 400, 404, 429],
"invalid_statuses": [403]
}
},
{
"title": "User App Keys",
"name": "user_app_keys",
"description": "View and manage Application Keys owned by the user.",
"resource": "Key Management",
"test": {
"endpoint": "/v2/current_user/application_keys/does-not-exist",
"method": "DELETE",
"valid_statuses": [200, 400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "org_app_keys_read",
"title": "Org App Keys Read",
"description": "View Application Keys owned by all users in the organization.",
"resource": "Key Management",
"test": {
"endpoint": "/v2/application_keys",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "org_app_keys_write",
"title": "Org App Keys Write",
"description": "Manage Application Keys owned by all users in the organization.",
"resource": "Key Management",
"test": {
"endpoint": "/v2/application_keys/does-not-exist",
"method": "DELETE",
"valid_statuses": [200, 400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "user_access_manage",
"title": "User Access Manage",
"description": "Disable users, manage user roles, manage SAML-to-role mappings, and configure logs restriction queries.",
"resource": "Users",
"test": {
"endpoint": "/v2/users/does-not-exist",
"method": "PATCH",
"valid_statuses": [200, 400, 429],
"invalid_statuses": [403]
}
},
{
"name": "synthetics_private_location_read",
"title": "Synthetics Private Locations Read",
"description": "View, search, and use Synthetics private locations.",
"resource": "Synthetics",
"test": {
"endpoint": "/v1/synthetics/private-locations/does-not-exit",
"method": "GET",
"valid_statuses": [200, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "synthetics_private_location_write",
"title": "Synthetics Private Locations Write",
"description": "Create and delete private locations in addition to having access to the associated installation guidelines.",
"resource": "Synthetics",
"test": {
"endpoint": "/v1/synthetics/private-locations/does-not-exit",
"method": "PUT",
"valid_statuses": [200, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "usage_read",
"title": "Usage Read",
"description": "View your organization's usage and usage attribution.",
"resource": "Usage Metering",
"test": {
"endpoint": "/v2/usage/hourly_usage",
"method": "GET",
"valid_statuses": [200, 400, 429],
"invalid_statuses": [403]
}
},
{
"name": "metric_tags_write",
"title": "Metric Tags Write",
"description": "Edit and save tag configurations for custom metrics.",
"resource": "Metrics",
"test": {
"endpoint": "/v2/metrics/does-not-exit/tags",
"method": "POST",
"valid_statuses": [200, 400, 429],
"invalid_statuses": [403]
}
},
{
"name": "audit_logs_read",
"title": "Audit Trail Read",
"description": "View Audit Trail in your organization.",
"resource": "Audit",
"test": {
"endpoint": "/v2/audit/events",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "api_keys_read",
"title": "API Keys Read",
"description": "List and retrieve the key values of all API Keys in your organization.",
"resource": "Key Management",
"test": {
"endpoint": "/v2/api_keys",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "api_keys_write",
"title": "API Keys Write",
"description": "Create and rename API Keys for your organization.",
"resource": "Key Management",
"test": {
"endpoint": "/v2/api_keys/does-not-exist",
"method": "PATCH",
"valid_statuses": [200, 400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "synthetics_global_variable_read",
"title": "Synthetics Global Variable Read",
"description": "View, search, and use Synthetics global variables.",
"resource": "Synthetics",
"test": {
"endpoint": "/v1/synthetics/variables",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "synthetics_global_variable_write",
"title": "Synthetics Global Variable Write",
"description": "Create, edit, and delete global variables for Synthetics.",
"resource": "Synthetics",
"test": {
"endpoint": "/v1/synthetics/variables",
"method": "POST",
"valid_statuses": [200, 400, 429],
"invalid_statuses": [403]
}
},
{
"name": "synthetics_read",
"title": "Synthetics Read",
"description": "List and view configured Synthetic tests and test results.",
"resource": "Synthetics",
"test": {
"endpoint": "/v1/synthetics/tests",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "synthetics_write",
"title": "Synthetics Write",
"description": "Create, edit, and delete Synthetic tests.",
"resource": "Synthetics",
"test": {
"endpoint": "/v1/synthetics/tests/mobile/does-not-exit",
"method": "PUT",
"valid_statuses": [200, 400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "synthetics_default_settings_read",
"title": "Synthetics Default Settings Read",
"description": "View the default settings for Synthetic Monitoring.",
"resource": "Synthetics",
"test": {
"endpoint": "/v1/synthetics/settings/default_locations",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "service_account_write",
"title": "Service Account Write",
"description": "Create, disable, and use Service Accounts in your organization.",
"resource": "Service Accounts",
"test": {
"endpoint": "/v2/service_accounts/does-not-exist/application_keys",
"method": "POST",
"valid_statuses": [200, 400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "apm_read",
"title": "APM Read",
"description": "Read and query APM and Trace Analytics.",
"resource": "APM",
"test": {
"endpoint": "/v2/apm/config/metrics",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "apm_retention_filter_read",
"title": "APM Retention Filters Read",
"description": "Read trace retention filters. A user with this permission can view the retention filters page, list of filters, their statistics, and creation info.",
"resource": "APM",
"test": {
"endpoint": "/v2/apm/config/retention-filters/should-not-exist",
"method": "GET",
"valid_statuses": [200, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "apm_retention_filter_write",
"title": "APM Retention Filters Write",
"description": "Create, edit, and delete trace retention filters. A user with this permission can create new retention filters, and update or delete to existing retention filters.",
"resource": "APM",
"test": {
"endpoint": "/v2/apm/config/retention-filters/should-not-exit",
"method": "DELETE",
"valid_statuses": [404, 429],
"invalid_statuses": [403]
}
},
{
"name": "rum_apps_write",
"title": "RUM Apps Write",
"description": "Create, edit, and delete RUM applications. Creating a RUM application automatically generates a Client Token. In order to create Client Tokens directly, a user needs the Client Tokens Write permission.",
"resource": "RUM",
"test": {
"endpoint": "/v2/rum/applications/does-not-exist",
"method": "DELETE",
"valid_statuses": [404, 429],
"invalid_statuses": [403]
}
},
{
"name": "data_scanner_read",
"title": "Data Scanner Read",
"description": "View Sensitive Data Scanner configurations and scanning results.",
"resource": "Sensitive Data Scanner",
"test": {
"endpoint": "/v2/sensitive-data-scanner/config/standard-patterns",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "data_scanner_write",
"title": "Data Scanner Write",
"description": "Edit Sensitive Data Scanner configurations.",
"resource": "Sensitive Data Scanner",
"test": {
"endpoint": "/v2/sensitive-data-scanner/config/groups/does-not-exist",
"method": "DELETE",
"valid_statuses": [404, 429],
"invalid_statuses": [403]
}
},
{
"name": "org_management",
"title": "Org Management",
"description": "Edit org configurations, including authentication and certain security preferences such as configuring SAML, renaming an org, configuring allowed login methods, creating child orgs, subscribing & unsubscribing from apps in the marketplace, and enabling & disabling Remote Configuration for the entire organization.",
"resource": "Organizations",
"test": {
"endpoint": "/v1/org",
"method": "GET",
"valid_statuses": [200, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "security_monitoring_filters_read",
"title": "Security Filters Read",
"description": "Read Security Filters.",
"resource": "Security Monitoring",
"test": {
"endpoint": "/v2/security_monitoring/configuration/security_filters",
"method": "GET",
"valid_statuses": [200, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "security_monitoring_filters_write",
"title": "Security Filters Write",
"description": "Create, edit, and delete Security Filters.",
"resource": "Security Monitoring",
"test": {
"endpoint": "/v2/security_monitoring/configuration/security_filters/does-not-exist",
"method": "DELETE",
"valid_statuses": [404, 429],
"invalid_statuses": [403]
}
},
{
"name": "incident_read",
"title": "Incidents Read",
"description": "View incidents in Datadog.",
"resource": "Incidents",
"test": {
"endpoint": "/v2/incidents",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "incident_write",
"title": "Incidents Write",
"description": "Create, view, and manage incidents in Datadog.",
"resource": "Incidents",
"test": {
"endpoint": "/v2/incidents/does-not-exist",
"method": "DELETE",
"valid_statuses": [404, 429],
"invalid_statuses": [403]
}
},
{
"name": "incident_settings_write",
"title": "Incident Settings Write",
"description": "Configure Incident Settings.",
"resource": "Incidents",
"test": {
"endpoint": "/v2/incidents/config/types/does-not-exist",
"method": "DELETE",
"valid_statuses": [400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "rum_apps_read",
"title": "RUM Apps Read",
"description": "View RUM Applications data.",
"resource": "RUM",
"test": {
"endpoint": "/v2/rum/applications",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "security_monitoring_notification_profiles_read",
"title": "Security Notification Rules Read",
"description": "Read Notification Rules.",
"resource": "Security Monitoring",
"test": {
"endpoint": "/v2/security/signals/notification_rules",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "security_monitoring_notification_profiles_write",
"title": "Security Notification Rules Write",
"description": "Create, edit, and delete Notification Rules.",
"resource": "Security Monitoring",
"test": {
"endpoint": "/v2/security/signals/notification_rules/does-not-exist",
"method": "DELETE",
"valid_statuses": [404, 429],
"invalid_statuses": [403]
}
},
{
"name": "apm_generate_metrics",
"title": "APM Generate Metrics",
"description": "Create custom metrics from spans.",
"resource": "APM",
"test": {
"endpoint": "/v2/apm/config/metrics/does-not-exist",
"method": "DELETE",
"valid_statuses": [404, 429],
"invalid_statuses": [403]
}
},
{
"name": "apm_pipelines_write",
"title": "APM Pipelines Write",
"description": "Add and change APM pipeline configurations.",
"resource": "APM",
"test": {
"endpoint": "/v2/apm/config/retention-filters/does-not-exist",
"method": "DELETE",
"valid_statuses": [404, 429],
"invalid_statuses": [403]
}
},
{
"name": "apm_pipelines_read",
"title": "APM Pipelines Read",
"description": "View APM pipeline configurations.",
"resource": "APM",
"test": {
"endpoint": "/v2/apm/config/retention-filters",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "observability_pipelines_read",
"title": "Observability Pipelines Read",
"description": "View pipelines in your organization.",
"resource": "Observability Pipelines",
"test": {
"endpoint": "/v2/remote_config/products/obs_pipelines/pipelines",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "workflows_read",
"title": "Workflows Read",
"description": "View workflows.",
"resource": "Workflows",
"test": {
"endpoint": "/v2/workflows/does-not-exist",
"method": "GET",
"valid_statuses": [200, 400, 429],
"invalid_statuses": [403]
}
},
{
"name": "workflows_write",
"title": "Workflows Write",
"description": "Create, edit, and delete workflows.",
"resource": "Workflows",
"test": {
"endpoint": "/v2/workflows/does-not-exist",
"method": "DELETE",
"valid_statuses": [400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "workflows_run",
"title": "Workflows Run",
"description": "Run workflows.",
"resource": "Workflows",
"test": {
"endpoint": "/v2/workflows/should-not-exist/instances",
"method": "POST",
"valid_statuses": [400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "connections_read",
"title": "Connections Read",
"description": "List and view available connections. Connections contain secrets that cannot be revealed.",
"resource": "Connections",
"test": {
"endpoint": "/v2/actions/connections/does-not-exist",
"method": "GET",
"valid_statuses": [200, 400, 429],
"invalid_statuses": [403]
}
},
{
"name": "connections_write",
"title": "Connections Write",
"description": "Create and delete connections.",
"resource": "Connections",
"test": {
"endpoint": "/v2/actions/connections/does-not-exist",
"method": "DELETE",
"valid_statuses": [400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "notebooks_read",
"title": "Notebooks Read",
"description": "View notebooks.",
"resource": "Notebooks",
"test": {
"endpoint": "/v1/notebooks",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "notebooks_write",
"title": "Notebooks Write",
"description": "Create and change notebooks.",
"resource": "Notebooks",
"test": {
"endpoint": "/v1/notebooks/does-not-exist",
"method": "DELETE",
"valid_statuses": [400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "aws_configurations_manage",
"title": "AWS Configurations Manage",
"description": "Add or remove but not edit AWS integration configurations.",
"resource": "Integrations",
"test": {
"endpoint": "/v2/integration/aws/accounts/does-not-exist",
"method": "DELETE",
"valid_statuses": [400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "azure_configurations_manage",
"title": "Azure Configurations Manage",
"description": "Add or remove but not edit Azure integration configurations.",
"resource": "Integrations",
"test": {
"endpoint": "/v1/integration/azure",
"method": "DELETE",
"valid_statuses": [400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "gcp_configurations_manage",
"title": "GCP Configurations Manage",
"description": "Add or remove but not edit GCP integration configurations.",
"resource": "Integrations",
"test": {
"endpoint": "/v2/integration/gcp/accounts/does-not-exist",
"method": "DELETE",
"valid_statuses": [400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "manage_integrations",
"title": "Integrations Manage",
"description": "Install, uninstall, and configure integrations.",
"resource": "Integrations",
"test": {
"endpoint": "/v2/integrations/cloudflare/accounts/does-not-exist",
"method": "DELETE",
"valid_statuses": [400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "slos_read",
"title": "SLOs Read",
"description": "View SLOs and status corrections.",
"resource": "SLOs",
"test": {
"endpoint": "/v1/slo",
"method": "GET",
"valid_statuses": [200, 429],
"invalid_statuses": [403]
}
},
{
"name": "slos_write",
"title": "SLOs Write",
"description": "Create, edit, and delete SLOs.",
"resource": "SLOs",
"test": {
"endpoint": "/v1/slo/does-not-exist",
"method": "DELETE",
"valid_statuses": [404, 429],
"invalid_statuses": [403]
}
},
{
"name": "slos_corrections",
"title": "SLOs Status Corrections",
"description": "Apply, edit, and delete SLO status corrections. A user with this permission can make status corrections, even if they do not have permission to edit those SLOs.",
"resource": "SLOs",
"test": {
"endpoint": "/v1/slo/correction",
"method": "POST",
"valid_statuses": [400, 429],
"invalid_statuses": [403]
}
},
{
"name": "monitor_config_policy_write",
"title": "Monitor Configuration Policy Write",
"description": "Create, update, and delete monitor configuration policies.",
"resource": "Monitors",
"test": {
"endpoint": "/v2/monitor/policy/does-not-exist",
"method": "DELETE",
"valid_statuses": [400, 404, 429],
"invalid_statuses": [403]
}
},
{
"name": "metrics_write",
"title": "Submit Metrics",
"description": "Submit custom metrics to Datadog.",
"resource": "Metrics"
},
{
"name": "logs_write",
"title": "Submit Logs",
"description": "Send logs to Datadog for indexing and processing.",
"resource": "Logs"
},
{
"name": "events_write",
"title": "Submit Events",
"description": "Post events to the Datadog event stream.",
"resource": "Events"
},
{
"name": "service_checks_write",
"title": "Submit Service Checks",
"description": "Send service check statuses to Datadog.",
"resource": "Service Checks"
}
]
================================================
FILE: pkg/analyzer/analyzers/digitalocean/digitalocean.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go digitalocean
package digitalocean
import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"sync"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
// to avoid rate limiting
const MAX_CONCURRENT_TESTS = 10
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeDigitalOcean }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, errors.New("missing key in credInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeDigitalOcean,
Metadata: nil,
Bindings: make([]analyzers.Binding, len(info.Permissions)),
}
resource := analyzers.Resource{
Name: info.User.Name,
FullyQualifiedName: info.User.UUID,
Type: "User",
Metadata: map[string]any{
"email": info.User.Email,
"status": info.User.Status,
},
}
for idx, permission := range info.Permissions {
result.Bindings[idx] = analyzers.Binding{
Resource: resource,
Permission: analyzers.Permission{
Value: permission,
},
}
}
return &result
}
//go:embed scopes.json
var scopesConfig []byte
type HttpStatusTest struct {
Endpoint string `json:"endpoint"`
Method string `json:"method"`
Payload interface{} `json:"payload"`
ValidStatuses []int `json:"valid_status_code"`
InvalidStatuses []int `json:"invalid_status_code"`
}
func StatusContains(status int, vals []int) bool {
for _, v := range vals {
if status == v {
return true
}
}
return false
}
func (h *HttpStatusTest) RunTest(cfg *config.Config, headers map[string]string) (bool, error) {
// If body data, marshal to JSON
var data io.Reader
if h.Payload != nil {
jsonData, err := json.Marshal(h.Payload)
if err != nil {
return false, err
}
data = bytes.NewBuffer(jsonData)
}
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest(h.Method, h.Endpoint, data)
if err != nil {
return false, err
}
// Add custom headers if provided
for key, value := range headers {
req.Header.Set(key, value)
}
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// Check response status code
switch {
case StatusContains(resp.StatusCode, h.ValidStatuses):
return true, nil
case StatusContains(resp.StatusCode, h.InvalidStatuses):
return false, nil
default:
return false, errors.New("error checking response status code")
}
}
type Scope struct {
Name string `json:"name"`
HttpTest HttpStatusTest `json:"test"`
}
func readInScopes() ([]Scope, error) {
var scopes []Scope
if err := json.Unmarshal(scopesConfig, &scopes); err != nil {
return nil, err
}
return scopes, nil
}
func checkPermissions(cfg *config.Config, key string) ([]string, error) {
scopes, err := readInScopes()
if err != nil {
return nil, fmt.Errorf("reading in scopes: %w", err)
}
var (
permissions = make([]string, 0, len(scopes))
mu sync.Mutex
wg sync.WaitGroup
slots = make(chan struct{}, MAX_CONCURRENT_TESTS)
errCh = make(chan error, 1)
)
for _, scope := range scopes {
wg.Add(1)
go func(scope Scope) {
defer wg.Done()
// acquire a slot
slots <- struct{}{}
defer func() { <-slots }()
status, err := scope.HttpTest.RunTest(cfg, map[string]string{"Authorization": "Bearer " + key})
if err != nil {
// send first error and ignore the rest
select {
case errCh <- fmt.Errorf("Scope %s: %w", scope.Name, err):
default:
}
return
}
if status {
mu.Lock()
permissions = append(permissions, scope.Name)
mu.Unlock()
}
}(scope)
}
// wait for all goroutines to finish or an error to occur
go func() {
wg.Wait()
close(errCh)
}()
if err := <-errCh; err != nil {
return nil, err
}
return permissions, nil
}
type user struct {
Email string `json:"email"`
Name string `json:"name"`
UUID string `json:"uuid"`
Status string `json:"status"`
}
type userJSON struct {
Account user `json:"account"`
}
func getUser(cfg *config.Config, token string) (*user, error) {
// Create new HTTP request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://api.digitalocean.com/v2/account", nil)
if err != nil {
return nil, err
}
// Add custom headers if provided
req.Header.Set("Authorization", "Bearer "+token)
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
// Decode response body
var response userJSON
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return nil, err
}
return &response.Account, nil
case http.StatusUnauthorized:
return nil, errors.New("invalid token")
default:
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
type SecretInfo struct {
User user
Permissions []string
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error : %s", err.Error())
return
}
color.Green("[!] Valid DigitalOcean API key\n\n")
color.Yellow("[i] User: %s (%s)\n\n", info.User.Name, info.User.Email)
printPermissions(info.Permissions)
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
var info = &SecretInfo{}
user, err := getUser(cfg, key)
if err != nil {
return nil, err
}
info.User = *user
permissions, err := checkPermissions(cfg, key)
if err != nil {
return nil, err
}
if len(permissions) == 0 {
return nil, fmt.Errorf("invalid DigitalOcean API key")
}
info.Permissions = permissions
return info, nil
}
func printPermissions(permissions []string) {
color.Yellow("[i] Permissions:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Permission"})
for _, permission := range permissions {
t.AppendRow(table.Row{color.GreenString(permission)})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/digitalocean/digitalocean_test.go
================================================
package digitalocean
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid digitalocean key",
key: testSecrets.MustGetField("DIGITALOCEAN_PERSONAL_ACCESS_TOKEN"),
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/digitalocean/expected_output.json
================================================
{"AnalyzerType":26,"Bindings":[{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"action:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"app:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"billing:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"block_storage:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"cdn_endpoint:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"certificate:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"container_registry:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"database:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"domain:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"domain_record:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"droplet:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"droplet_autoscale_pool:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"firewall:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"floating_ip:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"genai_agent:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"image:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"kubernetes:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"load_balancer:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"monitoring:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"namespace:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"one_click:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"project:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"region:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"reserved_ip:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"size:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"snapshot:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"ssh_key:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"tag:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"uptime:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"vpc:read","Parent":null}},{"Resource":{"Name":"sevoma","FullyQualifiedName":"f87d96c58bcc938176acebb04ac9450bbe113cca","Type":"User","Metadata":{"email":"sevoma@gmail.com","status":"active"},"Parent":null},"Permission":{"Value":"vpc_peering:read","Parent":null}}],"UnboundedResources":null,"Metadata":null}
================================================
FILE: pkg/analyzer/analyzers/digitalocean/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package digitalocean
import "errors"
type Permission int
const (
Invalid Permission = iota
OneClickRead Permission = iota
OneClickCreate Permission = iota
ActionRead Permission = iota
AppRead Permission = iota
AppCreate Permission = iota
AppUpdate Permission = iota
AppDelete Permission = iota
BillingRead Permission = iota
BlockStorageRead Permission = iota
BlockStorageCreate Permission = iota
BlockStorageDelete Permission = iota
CdnEndpointRead Permission = iota
CdnEndpointCreate Permission = iota
CdnEndpointUpdate Permission = iota
CdnEndpointDelete Permission = iota
CertificateRead Permission = iota
CertificateCreate Permission = iota
CertificateDelete Permission = iota
ContainerRegistryRead Permission = iota
ContainerRegistryCreate Permission = iota
DatabaseRead Permission = iota
DatabaseCreate Permission = iota
DatabaseUpdate Permission = iota
DatabaseDelete Permission = iota
DomainRecordRead Permission = iota
DomainRecordCreate Permission = iota
DomainRecordUpdate Permission = iota
DomainRecordDelete Permission = iota
DomainRead Permission = iota
DomainCreate Permission = iota
DomainDelete Permission = iota
DropletRead Permission = iota
DropletCreate Permission = iota
DropletDelete Permission = iota
DropletAutoscalePoolRead Permission = iota
DropletAutoscalePoolCreate Permission = iota
DropletAutoscalePoolUpdate Permission = iota
DropletAutoscalePoolDelete Permission = iota
FirewallRead Permission = iota
FirewallCreate Permission = iota
FirewallUpdate Permission = iota
FirewallDelete Permission = iota
FloatingIpRead Permission = iota
FloatingIpCreate Permission = iota
FloatingIpDelete Permission = iota
NamespaceRead Permission = iota
NamespaceCreate Permission = iota
NamespaceDelete Permission = iota
GenaiAgentRead Permission = iota
GenaiAgentCreate Permission = iota
GenaiAgentUpdate Permission = iota
GenaiAgentDelete Permission = iota
ImageRead Permission = iota
ImageCreate Permission = iota
ImageUpdate Permission = iota
ImageDelete Permission = iota
KubernetesRead Permission = iota
KubernetesCreate Permission = iota
KubernetesUpdate Permission = iota
KubernetesDelete Permission = iota
LoadBalancerRead Permission = iota
LoadBalancerCreate Permission = iota
LoadBalancerUpdate Permission = iota
LoadBalancerDelete Permission = iota
MonitoringRead Permission = iota
MonitoringCreate Permission = iota
MonitoringUpdate Permission = iota
MonitoringDelete Permission = iota
ProjectRead Permission = iota
ProjectCreate Permission = iota
ProjectUpdate Permission = iota
ProjectDelete Permission = iota
RegionRead Permission = iota
ReservedIpRead Permission = iota
ReservedIpCreate Permission = iota
ReservedIpDelete Permission = iota
SizeRead Permission = iota
SnapshotRead Permission = iota
SnapshotDelete Permission = iota
SshKeyRead Permission = iota
SshKeyCreate Permission = iota
SshKeyUpdate Permission = iota
SshKeyDelete Permission = iota
TagRead Permission = iota
TagCreate Permission = iota
TagDelete Permission = iota
UptimeRead Permission = iota
UptimeCreate Permission = iota
UptimeUpdate Permission = iota
UptimeDelete Permission = iota
VpcPeeringRead Permission = iota
VpcPeeringCreate Permission = iota
VpcPeeringUpdate Permission = iota
VpcPeeringDelete Permission = iota
VpcRead Permission = iota
VpcCreate Permission = iota
VpcUpdate Permission = iota
VpcDelete Permission = iota
)
var (
PermissionStrings = map[Permission]string{
OneClickRead: "one_click:read",
OneClickCreate: "one_click:create",
ActionRead: "action:read",
AppRead: "app:read",
AppCreate: "app:create",
AppUpdate: "app:update",
AppDelete: "app:delete",
BillingRead: "billing:read",
BlockStorageRead: "block_storage:read",
BlockStorageCreate: "block_storage:create",
BlockStorageDelete: "block_storage:delete",
CdnEndpointRead: "cdn_endpoint:read",
CdnEndpointCreate: "cdn_endpoint:create",
CdnEndpointUpdate: "cdn_endpoint:update",
CdnEndpointDelete: "cdn_endpoint:delete",
CertificateRead: "certificate:read",
CertificateCreate: "certificate:create",
CertificateDelete: "certificate:delete",
ContainerRegistryRead: "container_registry:read",
ContainerRegistryCreate: "container_registry:create",
DatabaseRead: "database:read",
DatabaseCreate: "database:create",
DatabaseUpdate: "database:update",
DatabaseDelete: "database:delete",
DomainRecordRead: "domain_record:read",
DomainRecordCreate: "domain_record:create",
DomainRecordUpdate: "domain_record:update",
DomainRecordDelete: "domain_record:delete",
DomainRead: "domain:read",
DomainCreate: "domain:create",
DomainDelete: "domain:delete",
DropletRead: "droplet:read",
DropletCreate: "droplet:create",
DropletDelete: "droplet:delete",
DropletAutoscalePoolRead: "droplet_autoscale_pool:read",
DropletAutoscalePoolCreate: "droplet_autoscale_pool:create",
DropletAutoscalePoolUpdate: "droplet_autoscale_pool:update",
DropletAutoscalePoolDelete: "droplet_autoscale_pool:delete",
FirewallRead: "firewall:read",
FirewallCreate: "firewall:create",
FirewallUpdate: "firewall:update",
FirewallDelete: "firewall:delete",
FloatingIpRead: "floating_ip:read",
FloatingIpCreate: "floating_ip:create",
FloatingIpDelete: "floating_ip:delete",
NamespaceRead: "namespace:read",
NamespaceCreate: "namespace:create",
NamespaceDelete: "namespace:delete",
GenaiAgentRead: "genai_agent:read",
GenaiAgentCreate: "genai_agent:create",
GenaiAgentUpdate: "genai_agent:update",
GenaiAgentDelete: "genai_agent:delete",
ImageRead: "image:read",
ImageCreate: "image:create",
ImageUpdate: "image:update",
ImageDelete: "image:delete",
KubernetesRead: "kubernetes:read",
KubernetesCreate: "kubernetes:create",
KubernetesUpdate: "kubernetes:update",
KubernetesDelete: "kubernetes:delete",
LoadBalancerRead: "load_balancer:read",
LoadBalancerCreate: "load_balancer:create",
LoadBalancerUpdate: "load_balancer:update",
LoadBalancerDelete: "load_balancer:delete",
MonitoringRead: "monitoring:read",
MonitoringCreate: "monitoring:create",
MonitoringUpdate: "monitoring:update",
MonitoringDelete: "monitoring:delete",
ProjectRead: "project:read",
ProjectCreate: "project:create",
ProjectUpdate: "project:update",
ProjectDelete: "project:delete",
RegionRead: "region:read",
ReservedIpRead: "reserved_ip:read",
ReservedIpCreate: "reserved_ip:create",
ReservedIpDelete: "reserved_ip:delete",
SizeRead: "size:read",
SnapshotRead: "snapshot:read",
SnapshotDelete: "snapshot:delete",
SshKeyRead: "ssh_key:read",
SshKeyCreate: "ssh_key:create",
SshKeyUpdate: "ssh_key:update",
SshKeyDelete: "ssh_key:delete",
TagRead: "tag:read",
TagCreate: "tag:create",
TagDelete: "tag:delete",
UptimeRead: "uptime:read",
UptimeCreate: "uptime:create",
UptimeUpdate: "uptime:update",
UptimeDelete: "uptime:delete",
VpcPeeringRead: "vpc_peering:read",
VpcPeeringCreate: "vpc_peering:create",
VpcPeeringUpdate: "vpc_peering:update",
VpcPeeringDelete: "vpc_peering:delete",
VpcRead: "vpc:read",
VpcCreate: "vpc:create",
VpcUpdate: "vpc:update",
VpcDelete: "vpc:delete",
}
StringToPermission = map[string]Permission{
"one_click:read": OneClickRead,
"one_click:create": OneClickCreate,
"action:read": ActionRead,
"app:read": AppRead,
"app:create": AppCreate,
"app:update": AppUpdate,
"app:delete": AppDelete,
"billing:read": BillingRead,
"block_storage:read": BlockStorageRead,
"block_storage:create": BlockStorageCreate,
"block_storage:delete": BlockStorageDelete,
"cdn_endpoint:read": CdnEndpointRead,
"cdn_endpoint:create": CdnEndpointCreate,
"cdn_endpoint:update": CdnEndpointUpdate,
"cdn_endpoint:delete": CdnEndpointDelete,
"certificate:read": CertificateRead,
"certificate:create": CertificateCreate,
"certificate:delete": CertificateDelete,
"container_registry:read": ContainerRegistryRead,
"container_registry:create": ContainerRegistryCreate,
"database:read": DatabaseRead,
"database:create": DatabaseCreate,
"database:update": DatabaseUpdate,
"database:delete": DatabaseDelete,
"domain_record:read": DomainRecordRead,
"domain_record:create": DomainRecordCreate,
"domain_record:update": DomainRecordUpdate,
"domain_record:delete": DomainRecordDelete,
"domain:read": DomainRead,
"domain:create": DomainCreate,
"domain:delete": DomainDelete,
"droplet:read": DropletRead,
"droplet:create": DropletCreate,
"droplet:delete": DropletDelete,
"droplet_autoscale_pool:read": DropletAutoscalePoolRead,
"droplet_autoscale_pool:create": DropletAutoscalePoolCreate,
"droplet_autoscale_pool:update": DropletAutoscalePoolUpdate,
"droplet_autoscale_pool:delete": DropletAutoscalePoolDelete,
"firewall:read": FirewallRead,
"firewall:create": FirewallCreate,
"firewall:update": FirewallUpdate,
"firewall:delete": FirewallDelete,
"floating_ip:read": FloatingIpRead,
"floating_ip:create": FloatingIpCreate,
"floating_ip:delete": FloatingIpDelete,
"namespace:read": NamespaceRead,
"namespace:create": NamespaceCreate,
"namespace:delete": NamespaceDelete,
"genai_agent:read": GenaiAgentRead,
"genai_agent:create": GenaiAgentCreate,
"genai_agent:update": GenaiAgentUpdate,
"genai_agent:delete": GenaiAgentDelete,
"image:read": ImageRead,
"image:create": ImageCreate,
"image:update": ImageUpdate,
"image:delete": ImageDelete,
"kubernetes:read": KubernetesRead,
"kubernetes:create": KubernetesCreate,
"kubernetes:update": KubernetesUpdate,
"kubernetes:delete": KubernetesDelete,
"load_balancer:read": LoadBalancerRead,
"load_balancer:create": LoadBalancerCreate,
"load_balancer:update": LoadBalancerUpdate,
"load_balancer:delete": LoadBalancerDelete,
"monitoring:read": MonitoringRead,
"monitoring:create": MonitoringCreate,
"monitoring:update": MonitoringUpdate,
"monitoring:delete": MonitoringDelete,
"project:read": ProjectRead,
"project:create": ProjectCreate,
"project:update": ProjectUpdate,
"project:delete": ProjectDelete,
"region:read": RegionRead,
"reserved_ip:read": ReservedIpRead,
"reserved_ip:create": ReservedIpCreate,
"reserved_ip:delete": ReservedIpDelete,
"size:read": SizeRead,
"snapshot:read": SnapshotRead,
"snapshot:delete": SnapshotDelete,
"ssh_key:read": SshKeyRead,
"ssh_key:create": SshKeyCreate,
"ssh_key:update": SshKeyUpdate,
"ssh_key:delete": SshKeyDelete,
"tag:read": TagRead,
"tag:create": TagCreate,
"tag:delete": TagDelete,
"uptime:read": UptimeRead,
"uptime:create": UptimeCreate,
"uptime:update": UptimeUpdate,
"uptime:delete": UptimeDelete,
"vpc_peering:read": VpcPeeringRead,
"vpc_peering:create": VpcPeeringCreate,
"vpc_peering:update": VpcPeeringUpdate,
"vpc_peering:delete": VpcPeeringDelete,
"vpc:read": VpcRead,
"vpc:create": VpcCreate,
"vpc:update": VpcUpdate,
"vpc:delete": VpcDelete,
}
PermissionIDs = map[Permission]int{
OneClickRead: 1,
OneClickCreate: 2,
ActionRead: 3,
AppRead: 4,
AppCreate: 5,
AppUpdate: 6,
AppDelete: 7,
BillingRead: 8,
BlockStorageRead: 9,
BlockStorageCreate: 10,
BlockStorageDelete: 11,
CdnEndpointRead: 12,
CdnEndpointCreate: 13,
CdnEndpointUpdate: 14,
CdnEndpointDelete: 15,
CertificateRead: 16,
CertificateCreate: 17,
CertificateDelete: 18,
ContainerRegistryRead: 19,
ContainerRegistryCreate: 20,
DatabaseRead: 21,
DatabaseCreate: 22,
DatabaseUpdate: 23,
DatabaseDelete: 24,
DomainRecordRead: 25,
DomainRecordCreate: 26,
DomainRecordUpdate: 27,
DomainRecordDelete: 28,
DomainRead: 29,
DomainCreate: 30,
DomainDelete: 31,
DropletRead: 32,
DropletCreate: 33,
DropletDelete: 34,
DropletAutoscalePoolRead: 35,
DropletAutoscalePoolCreate: 36,
DropletAutoscalePoolUpdate: 37,
DropletAutoscalePoolDelete: 38,
FirewallRead: 39,
FirewallCreate: 40,
FirewallUpdate: 41,
FirewallDelete: 42,
FloatingIpRead: 43,
FloatingIpCreate: 44,
FloatingIpDelete: 45,
NamespaceRead: 46,
NamespaceCreate: 47,
NamespaceDelete: 48,
GenaiAgentRead: 49,
GenaiAgentCreate: 50,
GenaiAgentUpdate: 51,
GenaiAgentDelete: 52,
ImageRead: 53,
ImageCreate: 54,
ImageUpdate: 55,
ImageDelete: 56,
KubernetesRead: 57,
KubernetesCreate: 58,
KubernetesUpdate: 59,
KubernetesDelete: 60,
LoadBalancerRead: 61,
LoadBalancerCreate: 62,
LoadBalancerUpdate: 63,
LoadBalancerDelete: 64,
MonitoringRead: 65,
MonitoringCreate: 66,
MonitoringUpdate: 67,
MonitoringDelete: 68,
ProjectRead: 69,
ProjectCreate: 70,
ProjectUpdate: 71,
ProjectDelete: 72,
RegionRead: 73,
ReservedIpRead: 74,
ReservedIpCreate: 75,
ReservedIpDelete: 76,
SizeRead: 77,
SnapshotRead: 78,
SnapshotDelete: 79,
SshKeyRead: 80,
SshKeyCreate: 81,
SshKeyUpdate: 82,
SshKeyDelete: 83,
TagRead: 84,
TagCreate: 85,
TagDelete: 86,
UptimeRead: 87,
UptimeCreate: 88,
UptimeUpdate: 89,
UptimeDelete: 90,
VpcPeeringRead: 91,
VpcPeeringCreate: 92,
VpcPeeringUpdate: 93,
VpcPeeringDelete: 94,
VpcRead: 95,
VpcCreate: 96,
VpcUpdate: 97,
VpcDelete: 98,
}
IdToPermission = map[int]Permission{
1: OneClickRead,
2: OneClickCreate,
3: ActionRead,
4: AppRead,
5: AppCreate,
6: AppUpdate,
7: AppDelete,
8: BillingRead,
9: BlockStorageRead,
10: BlockStorageCreate,
11: BlockStorageDelete,
12: CdnEndpointRead,
13: CdnEndpointCreate,
14: CdnEndpointUpdate,
15: CdnEndpointDelete,
16: CertificateRead,
17: CertificateCreate,
18: CertificateDelete,
19: ContainerRegistryRead,
20: ContainerRegistryCreate,
21: DatabaseRead,
22: DatabaseCreate,
23: DatabaseUpdate,
24: DatabaseDelete,
25: DomainRecordRead,
26: DomainRecordCreate,
27: DomainRecordUpdate,
28: DomainRecordDelete,
29: DomainRead,
30: DomainCreate,
31: DomainDelete,
32: DropletRead,
33: DropletCreate,
34: DropletDelete,
35: DropletAutoscalePoolRead,
36: DropletAutoscalePoolCreate,
37: DropletAutoscalePoolUpdate,
38: DropletAutoscalePoolDelete,
39: FirewallRead,
40: FirewallCreate,
41: FirewallUpdate,
42: FirewallDelete,
43: FloatingIpRead,
44: FloatingIpCreate,
45: FloatingIpDelete,
46: NamespaceRead,
47: NamespaceCreate,
48: NamespaceDelete,
49: GenaiAgentRead,
50: GenaiAgentCreate,
51: GenaiAgentUpdate,
52: GenaiAgentDelete,
53: ImageRead,
54: ImageCreate,
55: ImageUpdate,
56: ImageDelete,
57: KubernetesRead,
58: KubernetesCreate,
59: KubernetesUpdate,
60: KubernetesDelete,
61: LoadBalancerRead,
62: LoadBalancerCreate,
63: LoadBalancerUpdate,
64: LoadBalancerDelete,
65: MonitoringRead,
66: MonitoringCreate,
67: MonitoringUpdate,
68: MonitoringDelete,
69: ProjectRead,
70: ProjectCreate,
71: ProjectUpdate,
72: ProjectDelete,
73: RegionRead,
74: ReservedIpRead,
75: ReservedIpCreate,
76: ReservedIpDelete,
77: SizeRead,
78: SnapshotRead,
79: SnapshotDelete,
80: SshKeyRead,
81: SshKeyCreate,
82: SshKeyUpdate,
83: SshKeyDelete,
84: TagRead,
85: TagCreate,
86: TagDelete,
87: UptimeRead,
88: UptimeCreate,
89: UptimeUpdate,
90: UptimeDelete,
91: VpcPeeringRead,
92: VpcPeeringCreate,
93: VpcPeeringUpdate,
94: VpcPeeringDelete,
95: VpcRead,
96: VpcCreate,
97: VpcUpdate,
98: VpcDelete,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/digitalocean/permissions.yaml
================================================
permissions:
- one_click:read
- one_click:create
- action:read
- app:read
- app:create
- app:update
- app:delete
- billing:read
- block_storage:read
- block_storage:create
- block_storage:delete
- cdn_endpoint:read
- cdn_endpoint:create
- cdn_endpoint:update
- cdn_endpoint:delete
- certificate:read
- certificate:create
- certificate:delete
- container_registry:read
- container_registry:create
- database:read
- database:create
- database:update
- database:delete
- domain_record:read
- domain_record:create
- domain_record:update
- domain_record:delete
- domain:read
- domain:create
- domain:delete
- droplet:read
- droplet:create
- droplet:delete
- droplet_autoscale_pool:read
- droplet_autoscale_pool:create
- droplet_autoscale_pool:update
- droplet_autoscale_pool:delete
- firewall:read
- firewall:create
- firewall:update
- firewall:delete
- floating_ip:read
- floating_ip:create
- floating_ip:delete
- namespace:read
- namespace:create
- namespace:delete
- genai_agent:read
- genai_agent:create
- genai_agent:update
- genai_agent:delete
- image:read
- image:create
- image:update
- image:delete
- kubernetes:read
- kubernetes:create
- kubernetes:update
- kubernetes:delete
- load_balancer:read
- load_balancer:create
- load_balancer:update
- load_balancer:delete
- monitoring:read
- monitoring:create
- monitoring:update
- monitoring:delete
- project:read
- project:create
- project:update
- project:delete
- region:read
- reserved_ip:read
- reserved_ip:create
- reserved_ip:delete
- size:read
- snapshot:read
- snapshot:delete
- ssh_key:read
- ssh_key:create
- ssh_key:update
- ssh_key:delete
- tag:read
- tag:create
- tag:delete
- uptime:read
- uptime:create
- uptime:update
- uptime:delete
- vpc_peering:read
- vpc_peering:create
- vpc_peering:update
- vpc_peering:delete
- vpc:read
- vpc:create
- vpc:update
- vpc:delete
================================================
FILE: pkg/analyzer/analyzers/digitalocean/scopes.json
================================================
[
{
"name": "one_click:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/1-clicks",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "one_click:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/1-clicks/kubernetes",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "action:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/actions",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "app:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/apps",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "app:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/apps",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "app:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/apps/`nowaythisidcanexist",
"method": "PUT",
"valid_status_code": [400],
"invalid_status_code": [403]
}
},
{
"name": "app:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/apps/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [400],
"invalid_status_code": [403]
}
},
{
"name": "billing:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/customers/my/balance",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "block_storage:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/volumes",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "block_storage:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/volumes",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "block_storage:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/volumes/0000",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "cdn_endpoint:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/cdn/endpoints",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "cdn_endpoint:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/cdn/endpoints",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "cdn_endpoint:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/cdn/endpoints/`nowaythisidcanexist",
"method": "PUT",
"valid_status_code": [400],
"invalid_status_code": [403]
}
},
{
"name": "cdn_endpoint:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/cdn/endpoints/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [400],
"invalid_status_code": [403]
}
},
{
"name": "certificate:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/certificates",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "certificate:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/certificates",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "certificate:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/certificates/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "container_registry:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/registry",
"method": "GET",
"valid_status_code": [200, 404],
"invalid_status_code": [403]
}
},
{
"name": "container_registry:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/registry",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "database:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/databases",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "database:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/databases",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "database:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/databases/`nowaythisidcanexist/config",
"method": "PATCH",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "database:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/databases/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "domain_record:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/domains/`nowaythisdomaincanexist.com/records",
"method": "GET",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "domain_record:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/domains/`nowaythisdomaincanexist.com/records",
"method": "POST",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "domain_record:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/domains/`nowaythisdomaincanexist.com/records/`nowaythisidcanexist",
"method": "PUT",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "domain_record:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/domains/`nowaythisdomaincanexist.com/records/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "domain:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/domains",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "domain:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/domains",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "domain:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/domains/`nowaythisdomaincanexist.com",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "droplet:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/droplets",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "droplet:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/droplets",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "droplet:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/droplets/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "droplet_autoscale_pool:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/droplets/autoscale",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "droplet_autoscale_pool:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/droplets/autoscale",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "droplet_autoscale_pool:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/droplets/autoscale/0d3db13e-a604-4944-9827-7ec2642d32ac",
"method": "PUT",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "droplet_autoscale_pool:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/droplets/autoscale/0d3db13e-a604-4944-9827-7ec2642d32ac",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "firewall:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/firewalls",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "firewall:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/firewalls",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "firewall:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/firewalls/`nowaythisidcanexist",
"method": "PUT",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "firewall:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/firewalls/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "floating_ip:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/floating_ips",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "floating_ip:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/floating_ips",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "floating_ip:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/floating_ips/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "namespace:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/functions/namespaces",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "namespace:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/functions/namespaces",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "namespace:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/functions/namespaces/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "genai_agent:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/gen-ai/agents",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "genai_agent:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/gen-ai/agents",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "genai_agent:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/gen-ai/agents/`nowaythisidcanexist",
"method": "PUT",
"valid_status_code": [400],
"invalid_status_code": [403]
}
},
{
"name": "genai_agent:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/gen-ai/agents/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [400],
"invalid_status_code": [403]
}
},
{
"name": "image:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/images",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "image:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/images",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "image:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/images/`nowaythisidcanexist",
"method": "PUT",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "image:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/images/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "kubernetes:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/kubernetes/clusters",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "kubernetes:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/kubernetes/clusters",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "kubernetes:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/kubernetes/clusters/`nowaythisidcanexist",
"method": "PUT",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "kubernetes:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/kubernetes/clusters/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "load_balancer:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/load_balancers",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "load_balancer:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/load_balancers",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "load_balancer:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/load_balancers/`nowaythisidcanexist",
"method": "PUT",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "load_balancer:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/load_balancers/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "monitoring:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/monitoring/alerts",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "monitoring:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/monitoring/alerts",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "monitoring:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/monitoring/alerts/`nowaythisidcanexist",
"method": "PUT",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "monitoring:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/monitoring/alerts/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "project:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/projects",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "project:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/projects",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "project:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/projects/`nowaythisidcanexist",
"method": "PUT",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "project:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/projects/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "region:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/regions",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "reserved_ip:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/reserved_ips",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "reserved_ip:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/reserved_ips",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "reserved_ip:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/reserved_ips/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "size:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/sizes",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "snapshot:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/snapshots",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "snapshot:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/snapshots/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "ssh_key:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/account/keys",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "ssh_key:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/account/keys",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "ssh_key:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/account/keys/`nowaythisidcanexist",
"method": "PUT",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "ssh_key:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/account/keys/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "tag:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/tags",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "tag:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/tags",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "tag:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/tags/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "uptime:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/uptime/checks",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "uptime:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/uptime/checks",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "uptime:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/uptime/checks/`nowaythisidcanexist",
"method": "PUT",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "uptime:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/uptime/checks/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "vpc_peering:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/vpc_peerings",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "vpc_peering:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/vpc_peerings",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "vpc_peering:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/vpc_peerings/5a4981aa-9653-4bd1-bef5-d6bff52042e4",
"method": "PATCH",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "vpc_peering:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/vpc_peerings/5a4981aa-9653-4bd1-bef5-d6bff52042e4",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "vpc:read",
"test": {
"endpoint": "https://api.digitalocean.com/v2/vpcs",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "vpc:create",
"test": {
"endpoint": "https://api.digitalocean.com/v2/vpcs",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "vpc:update",
"test": {
"endpoint": "https://api.digitalocean.com/v2/vpcs/`nowaythisidcanexist",
"method": "PUT",
"valid_status_code": [404],
"invalid_status_code": [403]
}
},
{
"name": "vpc:delete",
"test": {
"endpoint": "https://api.digitalocean.com/v2/vpcs/`nowaythisidcanexist",
"method": "DELETE",
"valid_status_code": [404],
"invalid_status_code": [403]
}
}
]
================================================
FILE: pkg/analyzer/analyzers/dockerhub/dockerhub.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go dockerhub
package dockerhub
import (
"errors"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
// SecretInfo hold the information about the token generated from username and pat
type SecretInfo struct {
User User
Valid bool
Reference string
Permissions []string
Repositories []Repository
ExpiresIn string
Misc map[string]string
}
// User hold the information about user to whom the personal access token belongs
type User struct {
ID string
Username string
Email string
}
// Repository hold information about each repository the user can access
type Repository struct {
ID string
Name string
Type string
IsPrivate bool
StarCount int
PullCount int
}
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeDockerHub
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
username, exist := credInfo["username"]
if !exist {
return nil, errors.New("username not found in the credentials info")
}
pat, exist := credInfo["pat"]
if !exist {
return nil, errors.New("personal access token(PAT) not found in the credentials info")
}
info, err := AnalyzePermissions(a.Cfg, username, pat)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
// AnalyzePermissions will collect all the scopes assigned to token along with resource it can access
func AnalyzePermissions(cfg *config.Config, username, pat string) (*SecretInfo, error) {
// create the http client
client := analyzers.NewAnalyzeClientUnrestricted(cfg) // `/user/login` is a non-safe request
var secretInfo = &SecretInfo{}
// try to login and get jwt token
token, err := login(client, username, pat)
if err != nil {
return nil, err
}
if err := decodeTokenToSecretInfo(token, secretInfo); err != nil {
return nil, err
}
// fetch repositories using the jwt token and translate them to secret info
if err := fetchRepositories(client, username, token, secretInfo); err != nil {
return nil, err
}
// return secret info
return secretInfo, nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, username, pat string) {
info, err := AnalyzePermissions(cfg, username, pat)
if err != nil {
// just print the error in cli and continue as a partial success
color.Red("[x] Error : %s", err.Error())
}
if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}
if info.Valid {
color.Green("[!] Valid DockerHub Credentials\n\n")
// print user information
printUser(info.User)
// print permissions
printPermissions(info.Permissions)
// print repositories
printRepositories(info.Repositories)
color.Yellow("\n[i] Expires: %s", info.ExpiresIn)
}
}
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeDockerHub,
Metadata: map[string]any{"Valid_Key": info.Valid},
Bindings: make([]analyzers.Binding, len(info.Repositories)),
}
// extract information to create bindings and append to result bindings
for _, repo := range info.Repositories {
binding := analyzers.Binding{
Resource: analyzers.Resource{
Name: repo.Name,
FullyQualifiedName: repo.ID,
Type: repo.Type,
Metadata: map[string]any{
"is_private": repo.IsPrivate,
"pull_count": repo.PullCount,
"star_count": repo.StarCount,
},
},
Permission: analyzers.Permission{
// as all permissions are against repo, we assign the highest available permission
Value: assignHighestPermission(info.Permissions),
},
}
result.Bindings = append(result.Bindings, binding)
}
return &result
}
// cli print functions
func printUser(user User) {
color.Green("\n[i] User:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"ID", "Username", "Email"})
t.AppendRow(table.Row{color.GreenString(user.ID), color.GreenString(user.Username), color.GreenString(user.Email)})
t.Render()
}
func printPermissions(permissions []string) {
color.Yellow("[i] Permissions:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Permission"})
for _, permission := range permissions {
t.AppendRow(table.Row{color.GreenString(permission)})
}
t.Render()
}
func printRepositories(repos []Repository) {
color.Green("\n[i] Repositories:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Type", "ID(username/repo/repo_type/repo_name)", "Name", "Is Private", "Pull Count", "Star Count"})
for _, repo := range repos {
t.AppendRow(table.Row{color.GreenString(repo.Type), color.GreenString(repo.ID), color.GreenString(repo.Name),
color.GreenString("%t", repo.IsPrivate), color.GreenString("%d", repo.PullCount), color.GreenString("%d", repo.StarCount)})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/dockerhub/dockerhub_test.go
================================================
package dockerhub
import (
_ "embed"
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed result_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
username := testSecrets.MustGetField("DOCKERHUB_USERNAME")
pat := testSecrets.MustGetField("DOCKERHUB_PAT")
tests := []struct {
name string
username string
pat string
want []byte // JSON string
wantErr bool
}{
{
name: "valid dockerhub credentials",
username: username,
pat: pat,
want: expectedOutput,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"username": tt.username, "pat": tt.pat})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/dockerhub/helper.go
================================================
package dockerhub
import (
"errors"
"fmt"
"sort"
"time"
"github.com/golang-jwt/jwt/v5"
)
// permission hierarchy - always keep from highest permission to lowest
var permissionHierarchy = []string{"repo:admin", "repo:write", "repo:read", "repo:public_read"}
// precompute a ranking map for the ranking approach.
// lower index means higher permission.
var permissionRank = func() map[string]int {
rank := make(map[string]int, len(permissionHierarchy))
// loop over permissions hierarchy to assign index to each permission
// as hierarchy start from highest to lowest, the 0 index will be assigned to highest possible permission and n will be lowest possible permission
for i, perm := range permissionHierarchy {
rank[perm] = i
}
// return the rank map with indexed permissions
return rank
}()
// decodeTokenToSecretInfo decode the jwt token and add the information to secret info
func decodeTokenToSecretInfo(jwtToken string, secretInfo *SecretInfo) error {
type userClaims struct {
ID string `json:"uuid"`
Username string `json:"username"`
Email string `json:"email"`
}
type hubJwtClaims struct {
Scope string `json:"scope"`
HubClaims userClaims `json:"https://hub.docker.com"`
ExpiresIn int `json:"exp"`
jwt.RegisteredClaims
}
parser := jwt.NewParser()
token, _, err := parser.ParseUnverified(jwtToken, &hubJwtClaims{})
if err != nil {
return err
}
if claims, ok := token.Claims.(*hubJwtClaims); ok {
secretInfo.User = User{
ID: claims.HubClaims.ID,
Username: claims.HubClaims.Username,
Email: claims.HubClaims.Email,
}
secretInfo.ExpiresIn = humandReadableTime(claims.ExpiresIn)
secretInfo.Permissions = append(secretInfo.Permissions, claims.Scope)
secretInfo.Valid = true
return nil
}
return errors.New("failed to parse claims")
}
// repositoriesToSecretInfo translate repositories to secretInfo after sorting them
func repositoriesToSecretInfo(username string, repos *RepositoriesResponse, secretInfo *SecretInfo) {
// sort the repositories first
sortRepositories(repos)
for _, repo := range repos.Result {
secretInfo.Repositories = append(secretInfo.Repositories, Repository{
// as repositories does not have a unique key, we make one by combining multiple fields
ID: fmt.Sprintf("%s/repo/%s/%s", username, repo.Type, repo.Name), // e.g: user123/repo/image/repo1
Name: repo.Name,
Type: repo.Type,
IsPrivate: repo.IsPrivate,
StarCount: repo.StarCount,
PullCount: repo.PullCount,
})
}
}
/*
sortRepositories sort the repositories as following
private:
- pullcount(descending)
- starcount(descending)
public:
- pullcount(descending)
- starcount(descending)
*/
func sortRepositories(repos *RepositoriesResponse) {
sort.SliceStable(repos.Result, func(i, j int) bool {
a, b := repos.Result[i], repos.Result[j]
// prioritize private repositories over public
if a.IsPrivate != b.IsPrivate {
return a.IsPrivate
}
// sort by Pull Count (descending)
if a.PullCount != b.PullCount {
return a.PullCount > b.PullCount
}
// sort by Star Count (descending)
return a.StarCount > b.StarCount
})
}
// assignHighestPermission selects the highest available permission
func assignHighestPermission(permissions []string) string {
bestRank := len(permissionHierarchy)
bestPerm := ""
for _, perm := range permissions {
// check in indexes permissions
if rank, ok := permissionRank[perm]; ok {
// early exit if highest permission is found.
if rank == 0 {
return perm
}
if rank < bestRank {
bestRank = rank
bestPerm = perm
}
}
}
return bestPerm
}
// humandReadableTime converts seconds to days, hours, minutes, or seconds based on the value
func humandReadableTime(seconds int) string {
// Convert Unix timestamp to time.Time object
t := time.Unix(int64(seconds), 0)
// Format the time as "March 2" (Month Day format)
return t.Format("January 2, 2006")
}
================================================
FILE: pkg/analyzer/analyzers/dockerhub/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package dockerhub
import "errors"
type Permission int
const (
Invalid Permission = iota
RepoRead Permission = iota
RepoWrite Permission = iota
RepoAdmin Permission = iota
RepoPublicRead Permission = iota
)
var (
PermissionStrings = map[Permission]string{
RepoRead: "repo:read",
RepoWrite: "repo:write",
RepoAdmin: "repo:admin",
RepoPublicRead: "repo:public_read",
}
StringToPermission = map[string]Permission{
"repo:read": RepoRead,
"repo:write": RepoWrite,
"repo:admin": RepoAdmin,
"repo:public_read": RepoPublicRead,
}
PermissionIDs = map[Permission]int{
RepoRead: 1,
RepoWrite: 2,
RepoAdmin: 3,
RepoPublicRead: 4,
}
IdToPermission = map[int]Permission{
1: RepoRead,
2: RepoWrite,
3: RepoAdmin,
4: RepoPublicRead,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/dockerhub/permissions.yaml
================================================
permissions:
- repo:read
- repo:write
- repo:admin
- repo:public_read
================================================
FILE: pkg/analyzer/analyzers/dockerhub/requests.go
================================================
package dockerhub
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
)
// LoginResponse is the successful response from the /login API
type LoginResponse struct {
Token string `json:"token"`
}
// ErrorLoginResponse is the error response from the /login API
type ErrorLoginResponse struct {
Detail string `json:"detail"`
Login2FAToken string `json:"login_2fa_token"` // if login require 2FA authentication
}
// RepositoriesResponse is the /repositories/ response
type RepositoriesResponse struct {
Result []struct {
Name string `json:"name"`
Type string `json:"repository_type"`
IsPrivate bool `json:"is_private"`
StarCount int `json:"star_count"`
PullCount int `json:"pull_count"`
} `json:"results"`
}
// login call the /login api with username and jwt token and if successful retrieve the token string and return
func login(client *http.Client, username, pat string) (string, error) {
payload := strings.NewReader(fmt.Sprintf(`{"username": "%s", "password": "%s"}`, username, pat))
req, err := http.NewRequest(http.MethodPost, "https://hub.docker.com/v2/users/login", payload)
if err != nil {
return "", err
}
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
var token LoginResponse
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
return "", err
}
return token.Token, nil
case http.StatusUnauthorized:
var errorLogin ErrorLoginResponse
if err := json.NewDecoder(resp.Body).Decode(&errorLogin); err != nil {
return "", err
}
if errorLogin.Login2FAToken != "" {
// TODO: handle it more appropriately
return "", errors.New("valid credentials; account require 2fa authentication")
}
return "", errors.New(errorLogin.Detail)
default:
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
// fetchRepositories call /repositories/ API
func fetchRepositories(client *http.Client, username, token string, secretInfo *SecretInfo) error {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://hub.docker.com/v2/repositories/%s", username), http.NoBody)
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+token)
resp, err := client.Do(req)
if err != nil {
return err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
var repositories RepositoriesResponse
if err := json.NewDecoder(resp.Body).Decode(&repositories); err != nil {
return err
}
// translate repositories response to secretInfo
repositoriesToSecretInfo(username, &repositories, secretInfo)
return nil
case http.StatusUnauthorized, http.StatusForbidden:
// the token is valid and this shall never happen because the least scope a token can have is repo:public_read.
return nil
default:
return fmt.Errorf("unexpected status code: %d; while fetching repositories information", resp.StatusCode)
}
}
================================================
FILE: pkg/analyzer/analyzers/dockerhub/result_output.json
================================================
{
"AnalyzerType": 4,
"Bindings": [
{
"Resource": {
"Name": "test-private",
"FullyQualifiedName": "truffledockerman/repo/image/test-private",
"Type": "image",
"Metadata": {
"is_private": true,
"pull_count": 0,
"star_count": 0
},
"Parent": null
},
"Permission": {
"Value": "repo:admin",
"Parent": null
}
},
{
"Resource": {
"Name": "test",
"FullyQualifiedName": "truffledockerman/repo/image/test",
"Type": "image",
"Metadata": {
"is_private": false,
"pull_count": 0,
"star_count": 0
},
"Parent": null
},
"Permission": {
"Value": "repo:admin",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": {
"Valid_Key": true
}
}
================================================
FILE: pkg/analyzer/analyzers/dropbox/dropbox.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go dropbox
package dropbox
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
_ "embed"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
//go:embed scopes.json
var scopeConfigJson []byte
type Analyzer struct {
Cfg *config.Config
}
type PermissionStatus string
const (
StatusGranted PermissionStatus = "Granted"
StatusDenied PermissionStatus = "Denied"
StatusUnverified PermissionStatus = "Unverified"
)
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeDropbox
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
token, exist := credInfo["token"]
if !exist {
return nil, errors.New("token not found in credentials info")
}
info, err := AnalyzePermissions(a.Cfg, token)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, token string) {
info, err := AnalyzePermissions(cfg, token)
if err != nil {
color.Red("[x] Invalid Dropbox Token\n")
color.Red("[x] Error : %s", err.Error())
return
}
if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}
color.Green("[i] Valid Dropbox OAuth2 Credentials\n")
printAccountAndPermissions(info)
}
func AnalyzePermissions(cfg *config.Config, token string) (*secretInfo, error) {
// Dropbox API uses POST requests for all requests, so we need to use an unrestricted client
client := analyzers.NewAnalyzeClientUnrestricted(cfg)
scopeConfigMap, err := getScopeConfigMap()
if err != nil {
return nil, err
}
secretInfo := &secretInfo{}
accountInfoPermission := PermissionStrings[AccountInfoRead]
for _, perm := range PermissionStrings {
scopeDetails := scopeConfigMap.Scopes[perm]
status := StatusUnverified
if perm == accountInfoPermission {
// Account Info Read permission is always enabled
status = StatusGranted
}
secretInfo.Permissions = append(secretInfo.Permissions, accountPermission{
Name: perm,
Status: status,
Actions: scopeDetails.Actions,
})
}
if err := populateAccountInfo(client, secretInfo, token); err != nil {
return nil, err
}
if err := testAllPermissions(client, secretInfo, scopeConfigMap, token); err != nil {
return nil, err
}
return secretInfo, nil
}
func populateAccountInfo(client *http.Client, info *secretInfo, token string) error {
endpoint := "/2/users/get_current_account"
body, statusCode, err := callDropboxAPIEndpoint(client, endpoint, token)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
if err := json.Unmarshal([]byte(body), &info.Account); err != nil {
return fmt.Errorf("failed to unmarshal account info: %w", err)
}
return nil
default:
return fmt.Errorf("failed to validate scope. Status %d: %s", statusCode, body)
}
}
func testAllPermissions(client *http.Client, info *secretInfo, scopeConfigMap *scopeConfig, token string) error {
permissionStatuses := make(map[string]PermissionStatus)
for _, perm := range PermissionStrings {
scopeDetails := scopeConfigMap.Scopes[perm]
if _, ok := permissionStatuses[perm]; ok || scopeDetails.TestEndpoint == "" {
// Skip if the scope has already been determined or has no test endpoint
continue
}
if perm == PermissionStrings[Openid] {
// The OpenID permission can be validated using the "/2/users/get_current_account" endpoint
// If the response contains the "email" key, that implies that the "email" permission is also granted
// Similar case for the "given_name" key and the "profile" permission
body, statusCode, err := callDropboxAPIEndpoint(client, scopeDetails.TestEndpoint, token)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK, http.StatusConflict:
// The endpoint responds with 409 Conflict if the openid scope
// is granted but the email and profile scopes are not granted
permissionStatuses[perm] = StatusGranted
// Check for the "email" key in the response body
if strings.Contains(body, "\"email\":") {
permissionStatuses[PermissionStrings[Email]] = StatusGranted
} else {
permissionStatuses[PermissionStrings[Email]] = StatusDenied
}
// Check for the "given_name" key in the response body
if strings.Contains(body, "\"given_name\":") {
permissionStatuses[PermissionStrings[Profile]] = StatusGranted
} else {
permissionStatuses[PermissionStrings[Profile]] = StatusDenied
}
case http.StatusUnauthorized:
permissionStatuses[perm] = StatusDenied
permissionStatuses[PermissionStrings[Email]] = StatusDenied
permissionStatuses[PermissionStrings[Profile]] = StatusDenied
}
continue
}
isGranted, err := testPermission(client, scopeDetails.TestEndpoint, token)
if err != nil {
return err
}
if !isGranted {
permissionStatuses[perm] = StatusDenied
continue
}
permissionStatuses[perm] = StatusGranted
for _, impliedScope := range scopeDetails.ImpliedScopes {
permissionStatuses[impliedScope] = StatusGranted
}
}
for idx, permission := range info.Permissions {
permission.Status = permissionStatuses[permission.Name]
info.Permissions[idx] = permission
}
return nil
}
func testPermission(client *http.Client, testEndpoint string, token string) (bool, error) {
body, statusCode, err := callDropboxAPIEndpoint(client, testEndpoint, token)
if err != nil {
return false, err
}
switch statusCode {
case http.StatusUnauthorized:
return false, nil
case http.StatusBadRequest:
if strings.Contains(body, "does not have the required scope") {
return false, nil
}
if strings.Contains(body, "your request body is empty") {
return true, nil
}
}
return false, fmt.Errorf("failed to validate scope. Status %d: %s", statusCode, body)
}
func callDropboxAPIEndpoint(client *http.Client, endpoint string, token string) (string, int, error) {
baseURL := "https://api.dropboxapi.com"
req, err := http.NewRequest(http.MethodPost, baseURL+endpoint, nil)
if err != nil {
return "", 0, err
}
req.Header.Set("Authorization", "Bearer "+token)
res, err := client.Do(req)
if err != nil {
return "", 0, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return "", 0, fmt.Errorf("failed to read response body: %w", err)
}
return string(bodyBytes), res.StatusCode, nil
}
func getScopeConfigMap() (*scopeConfig, error) {
var scopeConfigMap scopeConfig
if err := json.Unmarshal(scopeConfigJson, &scopeConfigMap); err != nil {
return nil, errors.New("failed to unmarshal scopes.json: " + err.Error())
}
return &scopeConfigMap, nil
}
func secretInfoToAnalyzerResult(info *secretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
account := info.Account
accountID := account.AccountID
allPermissions := getValidatedPermissions(info)
resource := analyzers.Resource{
Name: fmt.Sprintf("%s %s", account.Name.GivenName, account.Name.Surname),
FullyQualifiedName: accountID,
Type: "account",
Metadata: map[string]any{
"email": account.Email,
"emailVerified": account.EmailVerified,
"disabled": account.Disabled,
"country": account.Country,
"accountType": account.AccountType.Tag,
},
}
analyzers.BindAllPermissions(resource, allPermissions...)
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeDropbox,
Metadata: nil,
Bindings: analyzers.BindAllPermissions(resource, allPermissions...),
}
return &result
}
func getValidatedPermissions(info *secretInfo) []analyzers.Permission {
permissions := []analyzers.Permission{}
for _, permission := range info.Permissions {
if permission.Status != StatusGranted {
continue
}
permissions = append(permissions, analyzers.Permission{
Value: permission.Name,
})
}
return permissions
}
func printAccountAndPermissions(info *secretInfo) {
color.Yellow("\n[i] Accounts Info:")
t1 := table.NewWriter()
t1.SetOutputMirror(os.Stdout)
t1.AppendHeader(table.Row{"ID", "Name", "Email", "Email Verified", "Disabled", "Country", "Account Type"})
emailVerified := "No"
disabled := "No"
if info.Account.EmailVerified {
emailVerified = "Yes"
}
if info.Account.Disabled {
disabled = "Yes"
}
t1.AppendRow(table.Row{
color.GreenString(info.Account.AccountID),
color.GreenString(info.Account.Name.GivenName + " " + info.Account.Name.Surname),
color.GreenString(info.Account.Email),
color.GreenString(emailVerified),
color.GreenString(disabled),
color.GreenString(info.Account.Country),
color.GreenString(info.Account.AccountType.Tag),
})
t1.SetOutputMirror(os.Stdout)
t1.Render()
color.Yellow("\n[i] Permissions:")
t2 := table.NewWriter()
t2.AppendHeader(table.Row{"Permission", "Access", "Actions"})
permissions := info.Permissions
for _, permission := range permissions {
access := "Denied"
permissionStatus := permission.Status
if permissionStatus == StatusGranted {
access = "Granted"
}
if permissionStatus == StatusUnverified {
access = "Unverified"
}
for idx, action := range permission.Actions {
permissionCell := ""
accessCell := ""
if idx == 0 {
permissionCell = color.GreenString(permission.Name)
accessCell = color.GreenString(access)
}
t2.AppendRow(table.Row{
permissionCell,
accessCell,
action,
})
}
t2.AppendSeparator()
}
t2.SetOutputMirror(os.Stdout)
t2.Render()
fmt.Printf("%s: https://www.dropbox.com/developers/documentation\n\n", color.GreenString("Ref"))
}
================================================
FILE: pkg/analyzer/analyzers/dropbox/dropbox_test.go
================================================
package dropbox
import (
_ "embed"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*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)
}
token := testSecrets.MustGetField("DROPBOX")
tests := []struct {
name string
secret string
want string
wantErr bool
}{
{
name: "valid dropbox credentials",
secret: token,
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{
"token": tt.secret,
})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
fmt.Println(string(gotJSON))
// compare the JSON strings
if string(gotJSON) != string(tt.want) {
// pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(tt.want, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/dropbox/expected_output.json
================================================
{"AnalyzerType":40,"Bindings":[{"Resource":{"Name":"Truffle Detectors","FullyQualifiedName":"dbid:AACfhSAzNq2rEGFtyIKeEchJumee8_A8Iq0","Type":"account","Metadata":{"accountType":"basic","country":"PK","disabled":false,"email":"detectors@trufflesec.com","emailVerified":true},"Parent":null},"Permission":{"Value":"accounts_info.read","Parent":null}},{"Resource":{"Name":"Truffle Detectors","FullyQualifiedName":"dbid:AACfhSAzNq2rEGFtyIKeEchJumee8_A8Iq0","Type":"account","Metadata":{"accountType":"basic","country":"PK","disabled":false,"email":"detectors@trufflesec.com","emailVerified":true},"Parent":null},"Permission":{"Value":"files.metadata.write","Parent":null}},{"Resource":{"Name":"Truffle Detectors","FullyQualifiedName":"dbid:AACfhSAzNq2rEGFtyIKeEchJumee8_A8Iq0","Type":"account","Metadata":{"accountType":"basic","country":"PK","disabled":false,"email":"detectors@trufflesec.com","emailVerified":true},"Parent":null},"Permission":{"Value":"sharing.read","Parent":null}},{"Resource":{"Name":"Truffle Detectors","FullyQualifiedName":"dbid:AACfhSAzNq2rEGFtyIKeEchJumee8_A8Iq0","Type":"account","Metadata":{"accountType":"basic","country":"PK","disabled":false,"email":"detectors@trufflesec.com","emailVerified":true},"Parent":null},"Permission":{"Value":"contacts.read","Parent":null}},{"Resource":{"Name":"Truffle Detectors","FullyQualifiedName":"dbid:AACfhSAzNq2rEGFtyIKeEchJumee8_A8Iq0","Type":"account","Metadata":{"accountType":"basic","country":"PK","disabled":false,"email":"detectors@trufflesec.com","emailVerified":true},"Parent":null},"Permission":{"Value":"files.content.read","Parent":null}},{"Resource":{"Name":"Truffle Detectors","FullyQualifiedName":"dbid:AACfhSAzNq2rEGFtyIKeEchJumee8_A8Iq0","Type":"account","Metadata":{"accountType":"basic","country":"PK","disabled":false,"email":"detectors@trufflesec.com","emailVerified":true},"Parent":null},"Permission":{"Value":"sharing.write","Parent":null}},{"Resource":{"Name":"Truffle Detectors","FullyQualifiedName":"dbid:AACfhSAzNq2rEGFtyIKeEchJumee8_A8Iq0","Type":"account","Metadata":{"accountType":"basic","country":"PK","disabled":false,"email":"detectors@trufflesec.com","emailVerified":true},"Parent":null},"Permission":{"Value":"contacts.write","Parent":null}},{"Resource":{"Name":"Truffle Detectors","FullyQualifiedName":"dbid:AACfhSAzNq2rEGFtyIKeEchJumee8_A8Iq0","Type":"account","Metadata":{"accountType":"basic","country":"PK","disabled":false,"email":"detectors@trufflesec.com","emailVerified":true},"Parent":null},"Permission":{"Value":"files.metadata.read","Parent":null}},{"Resource":{"Name":"Truffle Detectors","FullyQualifiedName":"dbid:AACfhSAzNq2rEGFtyIKeEchJumee8_A8Iq0","Type":"account","Metadata":{"accountType":"basic","country":"PK","disabled":false,"email":"detectors@trufflesec.com","emailVerified":true},"Parent":null},"Permission":{"Value":"files.content.write","Parent":null}},{"Resource":{"Name":"Truffle Detectors","FullyQualifiedName":"dbid:AACfhSAzNq2rEGFtyIKeEchJumee8_A8Iq0","Type":"account","Metadata":{"accountType":"basic","country":"PK","disabled":false,"email":"detectors@trufflesec.com","emailVerified":true},"Parent":null},"Permission":{"Value":"openid","Parent":null}},{"Resource":{"Name":"Truffle Detectors","FullyQualifiedName":"dbid:AACfhSAzNq2rEGFtyIKeEchJumee8_A8Iq0","Type":"account","Metadata":{"accountType":"basic","country":"PK","disabled":false,"email":"detectors@trufflesec.com","emailVerified":true},"Parent":null},"Permission":{"Value":"file_requests.read","Parent":null}},{"Resource":{"Name":"Truffle Detectors","FullyQualifiedName":"dbid:AACfhSAzNq2rEGFtyIKeEchJumee8_A8Iq0","Type":"account","Metadata":{"accountType":"basic","country":"PK","disabled":false,"email":"detectors@trufflesec.com","emailVerified":true},"Parent":null},"Permission":{"Value":"file_requests.write","Parent":null}},{"Resource":{"Name":"Truffle Detectors","FullyQualifiedName":"dbid:AACfhSAzNq2rEGFtyIKeEchJumee8_A8Iq0","Type":"account","Metadata":{"accountType":"basic","country":"PK","disabled":false,"email":"detectors@trufflesec.com","emailVerified":true},"Parent":null},"Permission":{"Value":"account_info.write","Parent":null}},{"Resource":{"Name":"Truffle Detectors","FullyQualifiedName":"dbid:AACfhSAzNq2rEGFtyIKeEchJumee8_A8Iq0","Type":"account","Metadata":{"accountType":"basic","country":"PK","disabled":false,"email":"detectors@trufflesec.com","emailVerified":true},"Parent":null},"Permission":{"Value":"profile","Parent":null}}],"UnboundedResources":null,"Metadata":null}
================================================
FILE: pkg/analyzer/analyzers/dropbox/models.go
================================================
package dropbox
type scopeConfig struct {
Scopes map[string]scope `json:"scopes"`
}
type scope struct {
TestEndpoint string `json:"test_endpoint"`
ImpliedScopes []string `json:"implied_scopes"`
Actions []string `json:"actions"`
}
type account struct {
AccountID string `json:"account_id"`
Name name `json:"name"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Disabled bool `json:"disabled"`
Country string `json:"country"`
AccountType accountType `json:"account_type"`
}
type accountType struct {
Tag string `json:".tag"`
}
type name struct {
GivenName string `json:"given_name"`
Surname string `json:"surname"`
}
type accountPermission struct {
Name string
Status PermissionStatus
Actions []string
}
type secretInfo struct {
Account account
Permissions []accountPermission
}
================================================
FILE: pkg/analyzer/analyzers/dropbox/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package dropbox
import "errors"
type Permission int
const (
Invalid Permission = iota
AccountInfoWrite Permission = iota
AccountInfoRead Permission = iota
FilesMetadataWrite Permission = iota
FilesMetadataRead Permission = iota
FilesContentWrite Permission = iota
FilesContentRead Permission = iota
SharingWrite Permission = iota
SharingRead Permission = iota
FileRequestsWrite Permission = iota
FileRequestsRead Permission = iota
ContactsWrite Permission = iota
ContactsRead Permission = iota
Openid Permission = iota
Profile Permission = iota
Email Permission = iota
)
var (
PermissionStrings = map[Permission]string{
AccountInfoWrite: "account_info.write",
AccountInfoRead: "account_info.read",
FilesMetadataWrite: "files.metadata.write",
FilesMetadataRead: "files.metadata.read",
FilesContentWrite: "files.content.write",
FilesContentRead: "files.content.read",
SharingWrite: "sharing.write",
SharingRead: "sharing.read",
FileRequestsWrite: "file_requests.write",
FileRequestsRead: "file_requests.read",
ContactsWrite: "contacts.write",
ContactsRead: "contacts.read",
Openid: "openid",
Profile: "profile",
Email: "email",
}
StringToPermission = map[string]Permission{
"account_info.write": AccountInfoWrite,
"account_info.read": AccountInfoRead,
"files.metadata.write": FilesMetadataWrite,
"files.metadata.read": FilesMetadataRead,
"files.content.write": FilesContentWrite,
"files.content.read": FilesContentRead,
"sharing.write": SharingWrite,
"sharing.read": SharingRead,
"file_requests.write": FileRequestsWrite,
"file_requests.read": FileRequestsRead,
"contacts.write": ContactsWrite,
"contacts.read": ContactsRead,
"openid": Openid,
"profile": Profile,
"email": Email,
}
PermissionIDs = map[Permission]int{
AccountInfoWrite: 1,
AccountInfoRead: 2,
FilesMetadataWrite: 3,
FilesMetadataRead: 4,
FilesContentWrite: 5,
FilesContentRead: 6,
SharingWrite: 7,
SharingRead: 8,
FileRequestsWrite: 9,
FileRequestsRead: 10,
ContactsWrite: 11,
ContactsRead: 12,
Openid: 13,
Profile: 14,
Email: 15,
}
IdToPermission = map[int]Permission{
1: AccountInfoWrite,
2: AccountInfoRead,
3: FilesMetadataWrite,
4: FilesMetadataRead,
5: FilesContentWrite,
6: FilesContentRead,
7: SharingWrite,
8: SharingRead,
9: FileRequestsWrite,
10: FileRequestsRead,
11: ContactsWrite,
12: ContactsRead,
13: Openid,
14: Profile,
15: Email,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/dropbox/permissions.yaml
================================================
permissions:
- account_info.write
- account_info.read
- files.metadata.write
- files.metadata.read
- files.content.write
- files.content.read
- sharing.write
- sharing.read
- file_requests.write
- file_requests.read
- contacts.write
- contacts.read
- openid
- profile
- email
================================================
FILE: pkg/analyzer/analyzers/dropbox/scopes.json
================================================
{
"scopes": {
"account_info.write": {
"test_endpoint": "/2/account/set_profile_photo",
"actions": [
"Set a user's profile photo"
]
},
"account_info.read": {
"test_endpoint": "/2/account/set_profile_photo",
"actions": [
"Validate user access token",
"Get a list of feature values for the current account",
"Get information about the current user's account",
"Get the space usage information for the current user's account"
]
},
"files.metadata.write": {
"test_endpoint": "/2/file_properties/properties/add",
"implied_scopes": [
"files.metadata.read"
],
"actions": [
"Add, update or remove property groups associated with files",
"Add, update or remove properties associated with files and templates",
"Add, update or remove templates associated with a user",
"Add or remove tags from items"
]
},
"files.metadata.read": {
"test_endpoint": "/2/file_properties/properties/search",
"actions": [
"Search across property templates for particular property field values",
"Get the schema for a specified template",
"Get the template identifiers for a team",
"Get the metadata for a file or folder",
"Get files, revisions, and folder contents",
"Monitor for file changes",
"Get tags from items",
"Get file metadata",
"Get user templates",
"Get user Paper docs"
]
},
"files.content.write": {
"test_endpoint": "/2/files/copy_v2",
"implied_scopes": [
"files.metadata.read"
],
"actions": [
"Add, update, move, or remove files",
"Add, update, move, or remove folders",
"Upload file content",
"Lock/unlock files for writing",
"Restore files to previous versions",
"Add, update, or archive Paper docs",
"Save URLs to Dropbox"
]
},
"files.content.read": {
"test_endpoint": "/2/files/get_file_lock_batch",
"actions": [
"Export or download files",
"Get lock information for files and folders",
"Get file previews",
"Stream file content",
"Get image file thumbnails",
"Export or download Paper docs"
]
},
"sharing.write": {
"test_endpoint": "/2/sharing/add_file_member",
"implied_scopes": [
"sharing.read"
],
"actions": [
"Add, update, or remove file members",
"Add, update, or remove folder members",
"Get status of all asynchronous jobs",
"Add, update, or remove shared links",
"Share or unshare folders",
"Add, update, or remove shared folder access policies",
"Mount or unmount folders",
"Add or remove users from Paper docs"
]
},
"sharing.read": {
"test_endpoint": "/2/sharing/get_file_metadata",
"actions": [
"Get file metadata",
"Get folder metadata",
"Get shared link metadata",
"Get file members",
"Get folder members",
"Get shared files",
"Get shared folders",
"Get mountable shared folders",
"Get shared links",
"Get information about the user's account",
"Get file and folder information for Paper doc",
"Get all users with Paper doc access"
]
},
"file_requests.write": {
"test_endpoint": "/2/file_requests/update",
"implied_scopes": [
"file_requests.read"
],
"actions": [
"Add, update, or remove file requests"
]
},
"file_requests.read": {
"test_endpoint": "/2/file_requests/list/continue",
"actions": [
"Get file requests",
"Get file request count"
]
},
"contacts.write": {
"test_endpoint": "/2/contacts/delete_manual_contacts_batch",
"implied_scopes": [
"contacts.read"
],
"actions": [
"Remove manually added contacts"
]
},
"contacts.read": {},
"openid": {
"test_endpoint": "/2/openid/userinfo",
"actions": [
"Get OpenID Connect user info"
]
},
"profile": {
"actions": [
"Get name in user info"
]
},
"email": {
"actions": [
"Get email address in user info"
]
}
}
}
================================================
FILE: pkg/analyzer/analyzers/elevenlabs/elevenlabs.go
================================================
package elevenlabs
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"slices"
"strings"
"sync"
"github.com/fatih/color"
"github.com/google/uuid"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
// SecretInfo hold information about key
type SecretInfo struct {
User User // the owner of key
Valid bool
Reference string
Permissions []string // list of Permissions assigned to the key
ElevenLabsResources []ElevenLabsResource // list of resources the key has access to
mu sync.RWMutex
}
// AppendPermission safely append new permission to secret info permissions list.
func (s *SecretInfo) AppendPermission(perm string) {
s.mu.Lock()
defer s.mu.Unlock()
s.Permissions = append(s.Permissions, perm)
}
// HasPermission safely read secret info permission list to check if passed permission exist in the list.
func (s *SecretInfo) HasPermission(perm Permission) bool {
s.mu.Lock()
defer s.mu.Unlock()
permissionString, _ := perm.ToString()
return slices.Contains(s.Permissions, permissionString)
}
// AppendResource safely append new resource to secret info elevenlabs resource list.
func (s *SecretInfo) AppendResource(resource ElevenLabsResource) {
s.mu.Lock()
defer s.mu.Unlock()
s.ElevenLabsResources = append(s.ElevenLabsResources, resource)
}
// User hold the information about user to whom the key belongs to
type User struct {
ID string
Name string
SubscriptionTier string
SubscriptionStatus string
}
// ElevenLabsResource hold information about the elevenlabs resource the key has access
type ElevenLabsResource struct {
ID string
Name string
Type string
Metadata map[string]string
Permission string
}
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeElevenLabs
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
// check if the `key` exist in the credentials info
key, exist := credInfo["key"]
if !exist {
return nil, errors.New("key not found in credentials info")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
// AnalyzePermissions check if key is valid and analyzes the permission for the key
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
// create http client
client := analyzers.NewAnalyzeClient(cfg)
var secretInfo = &SecretInfo{}
// fetch user information using the key
user, err := fetchUser(client, key)
if err != nil {
return nil, err
}
secretInfo.Valid = true
// if user is not nil, that means the key has user read permission. Set the user information in secret info user
// user can only be nil when the key is valid but it does not have a user read permission
if user != nil {
elevenLabsUserToSecretInfoUser(*user, secretInfo)
}
// get elevenlabs resources with permissions
if err := getElevenLabsResources(client, key, secretInfo); err != nil {
return nil, err
}
return secretInfo, nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
// just print the error in cli and continue as a partial success
color.Red("[x] Error : %s", err.Error())
}
if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}
if info.Valid {
color.Green("[!] Valid ElevenLabs API key\n\n")
// print user information
printUser(info.User)
// print permissions
printPermissions(info.Permissions)
// print resources
printElevenLabsResources(info.ElevenLabsResources)
color.Yellow("\n[i] Expires: Never")
}
}
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeElevenLabs,
Metadata: map[string]any{},
Bindings: make([]analyzers.Binding, 0),
}
// for resources to be uniquely identified, we need a unique id to be appended in resource fully qualified name
uniqueId := info.User.ID
if uniqueId == "" {
uniqueId = uuid.NewString()
}
// extract information from resource to create bindings and append to result bindings
for _, resource := range info.ElevenLabsResources {
// if resource has permission it is binded resource
if resource.Permission != "" {
binding := analyzers.Binding{
Resource: analyzers.Resource{
Name: resource.Name,
FullyQualifiedName: fmt.Sprintf("%s/%s/%s", uniqueId, resource.Type, resource.ID), // e.g: /Model/eleven_flash_v2_5
Type: resource.Type,
Metadata: map[string]any{}, // to avoid panic
},
Permission: analyzers.Permission{
Value: resource.Permission,
},
}
for key, value := range resource.Metadata {
binding.Resource.Metadata[key] = value
}
result.Bindings = append(result.Bindings, binding)
} else {
// if resource is missing permission it is an unbounded resource
unboundedResource := analyzers.Resource{
Name: resource.Name,
FullyQualifiedName: fmt.Sprintf("%s/%s/%s", uniqueId, resource.Type, resource.ID),
Type: resource.Type,
Metadata: map[string]any{},
}
for key, value := range resource.Metadata {
unboundedResource.Metadata[key] = value
}
result.UnboundedResources = append(result.UnboundedResources, unboundedResource)
}
}
result.Metadata["Valid_Key"] = info.Valid
return &result
}
// fetchUser fetch elevenlabs user information associated with the key
func fetchUser(client *http.Client, key string) (*User, error) {
response, statusCode, err := makeElevenLabsRequest(client, permissionToAPIMap[UserRead], http.MethodGet, key)
if err != nil {
return nil, err
}
switch statusCode {
case http.StatusOK:
var user UserResponse
if err := json.Unmarshal(response, &user); err != nil {
return nil, err
}
return &User{
ID: user.UserID,
Name: user.FirstName,
SubscriptionTier: user.Subscription.Tier,
SubscriptionStatus: user.Subscription.Status,
}, nil
case http.StatusUnauthorized:
var errorResp ErrorResponse
if err := json.Unmarshal(response, &errorResp); err != nil {
return nil, err
}
if errorResp.Detail.Status == InvalidAPIKey || errorResp.Detail.Status == NotVerifiable {
return nil, errors.New("invalid api key")
} else if errorResp.Detail.Status == MissingPermissions {
// key is missing user read permissions but is valid
return nil, nil
}
return nil, nil
default:
return nil, fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// elevenLabsUserToSecretInfoUser set the elevenlabs user information to secretInfo user
func elevenLabsUserToSecretInfoUser(user User, secretInfo *SecretInfo) {
secretInfo.User = user
// add user read scope to secret info
secretInfo.Permissions = append(secretInfo.Permissions, PermissionStrings[UserRead])
// map resource to secret info
// as user is accessible through a specific permission and has a unique id it is also a resource
secretInfo.ElevenLabsResources = append(secretInfo.ElevenLabsResources, ElevenLabsResource{
ID: user.ID,
Name: user.Name,
Type: "User",
Permission: PermissionStrings[UserRead],
})
}
/*
getElevenLabsResources gather resources the key can access
Note: The permissions in eleven labs is either Read or Read and Write. There is not separate permission for Write.
*/
func getElevenLabsResources(client *http.Client, key string, secretInfo *SecretInfo) error {
var (
aggregatedErrs = make([]string, 0)
errChan = make(chan error, 17) // buffer for 17 errors - one per API call
wg sync.WaitGroup
)
// history
wg.Add(1)
go func() {
defer wg.Done()
if err := getHistory(client, key, secretInfo); err != nil {
errChan <- err
}
if err := deleteHistory(client, key, secretInfo); err != nil {
errChan <- err
}
}()
// dubbings
wg.Add(1)
go func() {
defer wg.Done()
if err := deleteDubbing(client, key, secretInfo); err != nil {
errChan <- err
}
// if dubbing write permission was not added
if !secretInfo.HasPermission(DubbingWrite) {
if err := getDebugging(client, key, secretInfo); err != nil {
errChan <- err
}
}
}()
// voices
wg.Add(1)
go func() {
defer wg.Done()
if err := getVoices(client, key, secretInfo); err != nil {
errChan <- err
}
if err := deleteVoice(client, key, secretInfo); err != nil {
errChan <- err
}
}()
// projects
wg.Add(1)
go func() {
defer wg.Done()
if err := getProjects(client, key, secretInfo); err != nil {
errChan <- err
}
if err := deleteProject(client, key, secretInfo); err != nil {
errChan <- err
}
}()
// pronunciation dictionaries
wg.Add(1)
go func() {
defer wg.Done()
if err := getPronunciationDictionaries(client, key, secretInfo); err != nil {
errChan <- err
}
if err := removePronunciationDictionariesRule(client, key, secretInfo); err != nil {
errChan <- err
}
}()
// models
wg.Add(1)
go func() {
defer wg.Done()
if err := getModels(client, key, secretInfo); err != nil {
errChan <- err
}
}()
// audio native
wg.Add(1)
go func() {
defer wg.Done()
if err := updateAudioNativeProject(client, key, secretInfo); err != nil {
errChan <- err
}
}()
// workspace
wg.Add(1)
go func() {
defer wg.Done()
if err := deleteInviteFromWorkspace(client, key, secretInfo); err != nil {
errChan <- err
}
}()
// speech
wg.Add(1)
go func() {
defer wg.Done()
if err := textToSpeech(client, key, secretInfo); err != nil {
errChan <- err
}
// voice changer
if err := speechToSpeech(client, key, secretInfo); err != nil {
errChan <- err
}
}()
// audio isolation
wg.Add(1)
go func() {
defer wg.Done()
if err := audioIsolation(client, key, secretInfo); err != nil {
errChan <- err
}
}()
// agent
wg.Add(1)
go func() {
defer wg.Done()
// each agent can have a conversations which we get inside this function
if err := getAgents(client, key, secretInfo); err != nil {
errChan <- err
}
}()
// wait for all API calls to finish
wg.Wait()
close(errChan)
// collect all errors
for err := range errChan {
aggregatedErrs = append(aggregatedErrs, err.Error())
}
if len(aggregatedErrs) > 0 {
return errors.New(strings.Join(aggregatedErrs, ", "))
}
return nil
}
// cli print functions
func printUser(user User) {
color.Green("\n[i] User:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"ID", "Name", "Subscription Tier", "Subscription Status"})
t.AppendRow(table.Row{color.GreenString(user.ID), color.GreenString(user.Name), color.GreenString(user.SubscriptionTier), color.GreenString(user.SubscriptionStatus)})
t.Render()
}
func printPermissions(permissions []string) {
color.Yellow("[i] Permissions:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Permission"})
for _, permission := range permissions {
t.AppendRow(table.Row{color.GreenString(permission)})
}
t.Render()
}
func printElevenLabsResources(resources []ElevenLabsResource) {
color.Green("\n[i] Resources:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Resource Type", "Resource ID", "Resource Name", "Permission"})
for _, resource := range resources {
t.AppendRow(table.Row{color.GreenString(resource.Type), color.GreenString(resource.ID), color.GreenString(resource.Name), color.GreenString(resource.Permission)})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/elevenlabs/elevenlabs_test.go
================================================
package elevenlabs
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed result_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*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)
}
key := testSecrets.MustGetField("ELEVENLABS")
tests := []struct {
name string
key string
want []byte // JSON string
wantErr bool
}{
{
name: "valid ElevenLabs full access key",
key: key,
want: expectedOutput,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/elevenlabs/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package elevenlabs
import "errors"
type Permission int
const (
Invalid Permission = iota
TextToSpeech Permission = iota
SpeechToSpeech Permission = iota
AudioIsolation Permission = iota
DubbingRead Permission = iota
DubbingWrite Permission = iota
ProjectsRead Permission = iota
ProjectsWrite Permission = iota
AudioNativeRead Permission = iota
AudioNativeWrite Permission = iota
PronunciationDictionariesRead Permission = iota
PronunciationDictionariesWrite Permission = iota
VoicesRead Permission = iota
VoicesWrite Permission = iota
ModelsRead Permission = iota
SpeechHistoryRead Permission = iota
SpeechHistoryWrite Permission = iota
UserRead Permission = iota
WorkspaceRead Permission = iota
WorkspaceWrite Permission = iota
)
var (
PermissionStrings = map[Permission]string{
TextToSpeech: "text_to_speech",
SpeechToSpeech: "speech_to_speech",
AudioIsolation: "audio_isolation",
DubbingRead: "dubbing_read",
DubbingWrite: "dubbing_write",
ProjectsRead: "projects_read",
ProjectsWrite: "projects_write",
AudioNativeRead: "audio_native_read",
AudioNativeWrite: "audio_native_write",
PronunciationDictionariesRead: "pronunciation_dictionaries_read",
PronunciationDictionariesWrite: "pronunciation_dictionaries_write",
VoicesRead: "voices_read",
VoicesWrite: "voices_write",
ModelsRead: "models_read",
SpeechHistoryRead: "speech_history_read",
SpeechHistoryWrite: "speech_history_write",
UserRead: "user_read",
WorkspaceRead: "workspace_read",
WorkspaceWrite: "workspace_write",
}
StringToPermission = map[string]Permission{
"text_to_speech": TextToSpeech,
"speech_to_speech": SpeechToSpeech,
"audio_isolation": AudioIsolation,
"dubbing_read": DubbingRead,
"dubbing_write": DubbingWrite,
"projects_read": ProjectsRead,
"projects_write": ProjectsWrite,
"audio_native_read": AudioNativeRead,
"audio_native_write": AudioNativeWrite,
"pronunciation_dictionaries_read": PronunciationDictionariesRead,
"pronunciation_dictionaries_write": PronunciationDictionariesWrite,
"voices_read": VoicesRead,
"voices_write": VoicesWrite,
"models_read": ModelsRead,
"speech_history_read": SpeechHistoryRead,
"speech_history_write": SpeechHistoryWrite,
"user_read": UserRead,
"workspace_read": WorkspaceRead,
"workspace_write": WorkspaceWrite,
}
PermissionIDs = map[Permission]int{
TextToSpeech: 1,
SpeechToSpeech: 2,
AudioIsolation: 3,
DubbingRead: 4,
DubbingWrite: 5,
ProjectsRead: 6,
ProjectsWrite: 7,
AudioNativeRead: 8,
AudioNativeWrite: 9,
PronunciationDictionariesRead: 10,
PronunciationDictionariesWrite: 11,
VoicesRead: 12,
VoicesWrite: 13,
ModelsRead: 14,
SpeechHistoryRead: 15,
SpeechHistoryWrite: 16,
UserRead: 17,
WorkspaceRead: 18,
WorkspaceWrite: 19,
}
IdToPermission = map[int]Permission{
1: TextToSpeech,
2: SpeechToSpeech,
3: AudioIsolation,
4: DubbingRead,
5: DubbingWrite,
6: ProjectsRead,
7: ProjectsWrite,
8: AudioNativeRead,
9: AudioNativeWrite,
10: PronunciationDictionariesRead,
11: PronunciationDictionariesWrite,
12: VoicesRead,
13: VoicesWrite,
14: ModelsRead,
15: SpeechHistoryRead,
16: SpeechHistoryWrite,
17: UserRead,
18: WorkspaceRead,
19: WorkspaceWrite,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/elevenlabs/permissions.yaml
================================================
permissions:
- text_to_speech
- speech_to_speech
# - sound_generation
- audio_isolation
# - voice_generation
- dubbing_read
- dubbing_write
- projects_read
- projects_write
- audio_native_read
- audio_native_write
- pronunciation_dictionaries_read
- pronunciation_dictionaries_write
- voices_read
- voices_write
- models_read
# - models_write
- speech_history_read
- speech_history_write
- user_read
# - user_write
- workspace_read
- workspace_write
================================================
FILE: pkg/analyzer/analyzers/elevenlabs/requests.go
================================================
package elevenlabs
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"slices"
"strings"
)
// permissionToAPIMap contain the API endpoints for each scope/permission
// api docs: https://elevenlabs.io/docs/api-reference/introduction
var permissionToAPIMap = map[Permission]string{
TextToSpeech: "https://api.elevenlabs.io/v1/text-to-speech/%s", // require voice id
SpeechToSpeech: "https://api.elevenlabs.io/v1/speech-to-speech/%s", // require voice id
AudioIsolation: "https://api.elevenlabs.io/v1/audio-isolation",
DubbingRead: "https://api.elevenlabs.io/v1/dubbing/%s", // require dubbing id
DubbingWrite: "https://api.elevenlabs.io/v1/dubbing/%s", // require dubbing id
ProjectsRead: "https://api.elevenlabs.io/v1/projects",
ProjectsWrite: "https://api.elevenlabs.io/v1/projects/%s", // require project id
AudioNativeWrite: "https://api.elevenlabs.io/v1/audio-native/%s/content", // require project id
PronunciationDictionariesRead: "https://api.elevenlabs.io/v1/pronunciation-dictionaries",
PronunciationDictionariesWrite: "https://api.elevenlabs.io/v1/pronunciation-dictionaries/%s/remove-rules", // require pronunciation dictionary id
VoicesRead: "https://api.elevenlabs.io/v1/voices",
VoicesWrite: "https://api.elevenlabs.io/v1/voices/%s", // require voice id
ModelsRead: "https://api.elevenlabs.io/v1/models",
SpeechHistoryRead: "https://api.elevenlabs.io/v1/history",
SpeechHistoryWrite: "https://api.elevenlabs.io/v1/history/%s", // require history item id
UserRead: "https://api.elevenlabs.io/v1/user",
WorkspaceWrite: "https://api.elevenlabs.io/v1/workspace/invites",
}
var (
// not exist key
fakeID = "_thou_shalt_not_exist_"
// error statuses
NotVerifiable = "api_key_not_verifiable"
InvalidAPIKey = "invalid_api_key"
MissingPermissions = "missing_permissions"
DubbingNotFound = "dubbing_not_found"
ProjectNotFound = "project_not_found"
VoiceDoesNotExist = "voice_does_not_exist"
InvalidSubscription = "invalid_subscription"
PronunciationDictionaryNotFound = "pronunciation_dictionary_not_found"
InternalServerError = "internal_server_error"
InvalidProjectID = "invalid_project_id"
ModelNotFound = "model_not_found"
VoiceNotFound = "voice_not_found"
InvalidContent = "invalid_content"
)
// ErrorResponse is the error response for all APIs
type ErrorResponse struct {
Detail struct {
Status string `json:"status"`
} `json:"detail"`
}
// UserResponse is the /user API response
type UserResponse struct {
UserID string `json:"user_id"`
FirstName string `json:"first_name"`
Subscription struct {
Tier string `json:"tier"`
Status string `json:"status"`
} `json:"subscription"`
}
// HistoryResponse is the /history API response
type HistoryResponse struct {
History []struct {
ID string `json:"history_item_id"`
ModelID string `json:"model_id"`
VoiceID string `json:"voice_id"`
} `json:"history"`
}
// VoiceResponse is the /voices API response
type VoicesResponse struct {
Voices []struct {
ID string `json:"voice_id"`
Name string `json:"name"`
Category string `json:"category"`
} `json:"voices"`
}
// ProjectsResponse is the /projects API response
type ProjectsResponse struct {
Projects []struct {
ID string `json:"project_id"`
Name string `json:"name"`
State string `json:"state"`
AccessLevel string `json:"access_level"`
} `json:"projects"`
}
// PronunciationDictionaries is the /pronunciation-dictionaries API response
type PronunciationDictionariesResponse struct {
PronunciationDictionaries []struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"pronunciation_dictionaries"`
}
// Models is the /models API response
type ModelsResponse struct {
ID string `json:"model_id"`
Name string `json:"name"`
}
// AgentsResponse is the /agents API response
type AgentsResponse struct {
Agents []struct {
ID string `json:"agent_id"`
Name string `json:"name"`
AccessLevel string `json:"access_level"`
} `json:"agents"`
}
// ConversationResponse is the /conversation API response
type ConversationResponse struct {
Conversations []struct {
AgentID string `json:"agent_id"`
ID string `json:"conversation_id"`
Status string `json:"status"`
}
}
// getAPIUrl return the API Url mapped to the permission
func getAPIUrl(permission Permission) string {
apiUrl := permissionToAPIMap[permission]
if strings.Contains(apiUrl, "%s") {
return fmt.Sprintf(apiUrl, fakeID)
}
return apiUrl
}
// makeElevenLabsRequest send the API request to passed url with passed key as API Key and return response body and status code
func makeElevenLabsRequest(client *http.Client, url, method, key string) ([]byte, int, error) {
// create request
req, err := http.NewRequest(method, url, http.NoBody)
if err != nil {
return nil, 0, err
}
// add key in the header
req.Header.Add("xi-api-key", key)
resp, err := client.Do(req)
if err != nil {
return nil, 0, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
/*
the reason to translate body to byte and does not directly return http.Response
is if we return http.Response we cannot close the body in defer. If we do we will get an error
when reading body outside this function
*/
responseBodyByte, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, err
}
return responseBodyByte, resp.StatusCode, nil
}
// makeElevenLabsRequestWithPayload sends a POST/PATCH API request to the passed URL with the given key as the API Key
// and an optional payload. It returns the response body and status code.
func makeElevenLabsRequestWithPayload(client *http.Client, url, method, contentType, key string, payload []byte) ([]byte, int, error) {
// Create request with payload
req, err := http.NewRequest(method, url, bytes.NewBuffer(payload))
if err != nil {
return nil, 0, err
}
// Add headers
req.Header.Add("xi-api-key", key)
req.Header.Add("Content-Type", contentType)
// Send the request
resp, err := client.Do(req)
if err != nil {
return nil, 0, err
}
// ensure the response body is properly closed
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
// read the response body
responseBodyByte, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, err
}
return responseBodyByte, resp.StatusCode, nil
}
// getHistory get history item using the key passed and add them to secret info
func getHistory(client *http.Client, key string, secretInfo *SecretInfo) error {
response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(SpeechHistoryRead), http.MethodGet, key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var history HistoryResponse
if err := json.Unmarshal(response, &history); err != nil {
return err
}
// add history read scope to secret info
secretInfo.AppendPermission(PermissionStrings[SpeechHistoryRead])
// map resource to secret info
for _, historyItem := range history.History {
secretInfo.AppendResource(ElevenLabsResource{
ID: historyItem.ID,
Name: "", // no name
Type: "History",
Permission: PermissionStrings[SpeechHistoryRead],
})
}
return nil
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking history read scope", statusCode)
}
}
// deleteHistory try to delete a history item. The item must not exist.
func deleteHistory(client *http.Client, key string, secretInfo *SecretInfo) error {
response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(SpeechHistoryWrite), http.MethodDelete, key)
if err != nil {
return err
}
switch statusCode {
case http.StatusInternalServerError:
// for some reason if we send fake id and token has the permission, the history api return 500 error instead of 404
// issue opened in elevenlabs-docs: https://github.com/elevenlabs/elevenlabs-docs/issues/649
return handleErrorStatus(response, PermissionStrings[SpeechHistoryWrite], secretInfo, InternalServerError)
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking history write scope", statusCode)
}
}
// deleteDubbing try to delete a dubbing. The item must not exist.
func deleteDubbing(client *http.Client, key string, secretInfo *SecretInfo) error {
response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(DubbingWrite), http.MethodDelete, key)
if err != nil {
return err
}
switch statusCode {
case http.StatusNotFound:
// as we send fake id, if permission is assigned to token we must get 404 dubbing not found
if err := handleErrorStatus(response, PermissionStrings[DubbingWrite], secretInfo, DubbingNotFound); err != nil {
return err
}
// add read scope of dubbing to avoid get dubbing api call
secretInfo.AppendPermission(PermissionStrings[DubbingRead])
return nil
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking dubbing write scope", statusCode)
}
}
// getDebugging try to get a dubbing. The item must not exist.
func getDebugging(client *http.Client, key string, secretInfo *SecretInfo) error {
response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(DubbingRead), http.MethodGet, key)
if err != nil {
return err
}
switch statusCode {
case http.StatusNotFound:
// as we send fake id, if permission is assigned to token we must get 404 dubbing not found
return handleErrorStatus(response, PermissionStrings[DubbingRead], secretInfo, DubbingNotFound)
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking dubbing read scope", statusCode)
}
}
// getVoices get list of voices using the key passed and add them to secret info
func getVoices(client *http.Client, key string, secretInfo *SecretInfo) error {
response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(VoicesRead), http.MethodGet, key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var voices VoicesResponse
if err := json.Unmarshal(response, &voices); err != nil {
return err
}
// add voices read scope to secret info
secretInfo.AppendPermission(PermissionStrings[VoicesRead])
// map resource to secret info
for _, voice := range voices.Voices {
secretInfo.AppendResource(ElevenLabsResource{
ID: voice.ID,
Name: voice.Name,
Type: "Voice",
Permission: PermissionStrings[VoicesRead],
Metadata: map[string]string{
"category": voice.Category,
},
})
}
return nil
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking voice read scope", statusCode)
}
}
// deleteVoice try to delete a voice. The item must not exist.
func deleteVoice(client *http.Client, key string, secretInfo *SecretInfo) error {
response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(VoicesWrite), http.MethodDelete, key)
if err != nil {
return err
}
switch statusCode {
case http.StatusBadRequest:
// if permission was assigned to scope we should get 400 error with voice not found status
return handleErrorStatus(response, PermissionStrings[VoicesWrite], secretInfo, VoiceDoesNotExist)
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking voice write scope", statusCode)
}
}
// getProjects get list of projects using the key passed and add them to secret info
func getProjects(client *http.Client, key string, secretInfo *SecretInfo) error {
response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(ProjectsRead), http.MethodGet, key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var projects ProjectsResponse
if err := json.Unmarshal(response, &projects); err != nil {
return err
}
// add project read scope to secret info
secretInfo.AppendPermission(PermissionStrings[ProjectsRead])
// map resource to secret info
for _, project := range projects.Projects {
secretInfo.AppendResource(ElevenLabsResource{
ID: project.ID,
Name: project.Name,
Type: "Project",
Permission: PermissionStrings[ProjectsRead],
Metadata: map[string]string{
"state": project.State,
"access level": project.AccessLevel, // access level of project
},
})
}
return nil
case http.StatusForbidden:
// if token has the permission but trail is free, projects are not accessible
return handleErrorStatus(response, PermissionStrings[ProjectsRead], secretInfo, InvalidSubscription)
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking projects read scope", statusCode)
}
}
// deleteProject try to delete a project. The item must not exist.
func deleteProject(client *http.Client, key string, secretInfo *SecretInfo) error {
response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(ProjectsWrite), http.MethodDelete, key)
if err != nil {
return err
}
switch statusCode {
case http.StatusBadRequest:
// if permission was assigned to token we should get 400 error with project not found status
return handleErrorStatus(response, PermissionStrings[ProjectsWrite], secretInfo, ProjectNotFound)
case http.StatusForbidden:
// if token has the permission but trail is free, projects are not accessible
return handleErrorStatus(response, PermissionStrings[ProjectsWrite], secretInfo, InvalidSubscription)
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking project write scope", statusCode)
}
}
// getPronunciationDictionaries get list of pronunciation dictionaries using the key passed and add them to secret info
func getPronunciationDictionaries(client *http.Client, key string, secretInfo *SecretInfo) error {
response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(PronunciationDictionariesRead), http.MethodGet, key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var PDs PronunciationDictionariesResponse
if err := json.Unmarshal(response, &PDs); err != nil {
return err
}
// add voices read scope to secret info
secretInfo.AppendPermission(PermissionStrings[PronunciationDictionariesRead])
// map resource to secret info
for _, pd := range PDs.PronunciationDictionaries {
secretInfo.AppendResource(ElevenLabsResource{
ID: pd.ID,
Name: pd.Name,
Type: "Pronunciation Dictionary",
Permission: PermissionStrings[PronunciationDictionariesRead],
})
}
return nil
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking pronunciation dictionaries read scope", statusCode)
}
}
// removePronunciationDictionariesRule try to remove a rule from pronunciation dictionaries. The item must not exist.
func removePronunciationDictionariesRule(client *http.Client, key string, secretInfo *SecretInfo) error {
// send empty list of rule strings
payload := map[string]interface{}{
"rule_strings": []string{""},
}
payloadBytes, _ := json.Marshal(payload)
response, statusCode, err := makeElevenLabsRequestWithPayload(client, getAPIUrl(PronunciationDictionariesWrite), http.MethodPost,
"application/json", key, payloadBytes)
if err != nil {
return err
}
switch statusCode {
case http.StatusNotFound:
// if permission was assigned to token we should get 404 error with pronunciation_dictionary_not_found status
return handleErrorStatus(response, PermissionStrings[PronunciationDictionariesWrite], secretInfo, PronunciationDictionaryNotFound)
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking pronunciation dictionary write scope", statusCode)
}
}
// getModels list models using the key passed and add them to secret info
func getModels(client *http.Client, key string, secretInfo *SecretInfo) error {
response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(ModelsRead), http.MethodGet, key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var models []ModelsResponse
if err := json.Unmarshal(response, &models); err != nil {
return err
}
// add models read scope to secret info
secretInfo.AppendPermission(PermissionStrings[ModelsRead])
// map resource to secret info
for _, model := range models {
secretInfo.AppendResource(ElevenLabsResource{
ID: model.ID,
Name: model.Name,
Type: "Model",
Permission: PermissionStrings[ModelsRead],
})
}
return nil
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking models read scope", statusCode)
}
}
// updateAudioNativeProject try to update a project content. The item must not exist.
func updateAudioNativeProject(client *http.Client, key string, secretInfo *SecretInfo) error {
// create a buffer to hold the multipart form data
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// add required fields to multipart form body
_ = writer.WriteField("auto_convert", "false")
_ = writer.WriteField("auto_publish", "false")
// close the writer
_ = writer.Close()
response, statusCode, err := makeElevenLabsRequestWithPayload(client, getAPIUrl(AudioNativeWrite), http.MethodPost,
writer.FormDataContentType(), key, body.Bytes())
if err != nil {
return err
}
switch statusCode {
case http.StatusBadRequest:
// if the permission is assigned to token, the api should return 400 with invalid project id
if err := handleErrorStatus(response, PermissionStrings[AudioNativeWrite], secretInfo, InvalidProjectID); err != nil {
return err
}
// add read permission as no separate API exist to check read audio native permission
secretInfo.AppendPermission(PermissionStrings[AudioNativeRead])
return nil
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking audio native write scope", statusCode)
}
}
// deleteInviteFromWorkspace try to remove a invite from workspace. The item must not exist.
func deleteInviteFromWorkspace(client *http.Client, key string, secretInfo *SecretInfo) error {
// send fake email in payload
payload := map[string]interface{}{
"email": fakeID + "@example.com",
}
payloadBytes, _ := json.Marshal(payload)
response, statusCode, err := makeElevenLabsRequestWithPayload(client, getAPIUrl(WorkspaceWrite), http.MethodDelete,
"application/json", key, payloadBytes)
if err != nil {
return err
}
switch statusCode {
case http.StatusInternalServerError:
// for some reason if we send fake email and token has the permission, the workspace invite api return 500 error instead of 404
if err := handleErrorStatus(response, PermissionStrings[WorkspaceWrite], secretInfo, InternalServerError); err != nil {
return err
}
// add read permission as no separate API exist to check workspace read permission
secretInfo.AppendPermission(PermissionStrings[WorkspaceRead])
return nil
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking workspace write scope", statusCode)
}
}
// textToSpeech try to convert text to speech. The model id and voice id is fake so it actually never happens.
func textToSpeech(client *http.Client, key string, secretInfo *SecretInfo) error {
// send fake model id in payload
payload := map[string]interface{}{
"text": "This is trufflehog trying to check text to speech permission of the token",
"model_id": fakeID,
}
payloadBytes, _ := json.Marshal(payload)
response, statusCode, err := makeElevenLabsRequestWithPayload(client, getAPIUrl(TextToSpeech), http.MethodPost,
"application/json", key, payloadBytes)
if err != nil {
return err
}
switch statusCode {
case http.StatusBadRequest:
// if permission is assigned to token, error status will be either model not found or voice not found as we sent both fake ;)
return handleErrorStatus(response, PermissionStrings[TextToSpeech], secretInfo, ModelNotFound, VoiceNotFound)
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking text to speech scope", statusCode)
}
}
// speechToSpeech try to change a voice in speech. The model id and voice id is fake so it actually never happens.
func speechToSpeech(client *http.Client, key string, secretInfo *SecretInfo) error {
// create a buffer to hold the multipart form data
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// add required fields to multipart form body
_ = writer.WriteField("model_id", fakeID)
_ = writer.WriteField("seed", "1")
_ = writer.WriteField("remove_background_noise", "false")
audio, _ := writer.CreateFormFile("audio", "")
_, _ = audio.Write([]byte("This is example fake audio for api call"))
// close the writer
_ = writer.Close()
response, statusCode, err := makeElevenLabsRequestWithPayload(client, getAPIUrl(SpeechToSpeech), http.MethodPost,
writer.FormDataContentType(), key, body.Bytes())
if err != nil {
return err
}
switch statusCode {
case http.StatusBadRequest:
return handleErrorStatus(response, PermissionStrings[SpeechToSpeech], secretInfo, InvalidContent)
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking speech to speech scope", statusCode)
}
}
// audioIsolation try to remove background noise from a voice. The file will be corrupted so it should return an error.
func audioIsolation(client *http.Client, key string, secretInfo *SecretInfo) error {
// create a buffer to hold the multipart form data
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
audio, _ := writer.CreateFormFile("audio", "")
_, _ = audio.Write([]byte("This is example fake audio for api call"))
// close the writer
_ = writer.Close()
response, statusCode, err := makeElevenLabsRequestWithPayload(client, getAPIUrl(AudioIsolation), http.MethodPost,
writer.FormDataContentType(), key, body.Bytes())
if err != nil {
return err
}
switch statusCode {
case http.StatusBadRequest:
return handleErrorStatus(response, PermissionStrings[AudioIsolation], secretInfo, InvalidContent)
case http.StatusUnauthorized:
return handleErrorStatus(response, "", secretInfo, MissingPermissions)
default:
return fmt.Errorf("unexpected status code: %d while checking audio isolation speech scope", statusCode)
}
}
/*
getAgents get all user agents which are not bound with any permission
call APIs in pattern: agents->conversation
*/
func getAgents(client *http.Client, key string, secretInfo *SecretInfo) error {
response, statusCode, err := makeElevenLabsRequest(client, "https://api.elevenlabs.io/v1/convai/agents", http.MethodGet, key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var agents AgentsResponse
if err := json.Unmarshal(response, &agents); err != nil {
return err
}
// map resource to secret info
for _, agent := range agents.Agents {
resource := ElevenLabsResource{
ID: agent.ID,
Name: agent.Name,
Type: "Agent",
Permission: "", // not binded with any permission
Metadata: map[string]string{
"access level": agent.AccessLevel,
},
}
secretInfo.AppendResource(resource)
// get agent conversations
if err := getConversation(client, key, agent.ID, secretInfo); err != nil {
return err
}
}
return nil
default:
return fmt.Errorf("unexpected status code: %d while checking models read scope", statusCode)
}
}
// getConversation list all agent conversations using the key and agentID passed and add them to secret info
func getConversation(client *http.Client, key, agentID string, secretInfo *SecretInfo) error {
apiUrl := fmt.Sprintf("https://api.elevenlabs.io/v1/convai/conversations?agent_id=%s", agentID)
response, statusCode, err := makeElevenLabsRequest(client, apiUrl, http.MethodGet, key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var conversations ConversationResponse
if err := json.Unmarshal(response, &conversations); err != nil {
return err
}
// map resource to secret info
for _, conversation := range conversations.Conversations {
secretInfo.AppendResource(ElevenLabsResource{
ID: conversation.ID,
Name: "", // no name
Type: "Conversation",
Permission: "", // not binded with any permission
Metadata: map[string]string{
"status": conversation.Status,
},
})
}
return nil
default:
return fmt.Errorf("unexpected status code: %d while checking models read scope", statusCode)
}
}
// handleErrorStatus handle error response, check if expected error status is in the response and add require permission to secret info
// this is used in case where we expect error response with specific status mostly in write calls
func handleErrorStatus(response []byte, permissionToAdd string, secretInfo *SecretInfo, expectedErrStatuses ...string) error {
// check if status in response is what is expected to be
ok, err := checkErrorStatus(response, expectedErrStatuses...)
if err != nil {
return err
}
// if permission to add was passed and it was expected error status add the permission
if permissionToAdd != "" && ok {
secretInfo.AppendPermission(permissionToAdd)
} else if permissionToAdd != "" && !ok {
// if permission to add was passed and it was unexpected error status - return error
return errors.New("unexpected error response")
}
return nil
}
// checkErrorStatus check if any of expected error status exist in actual API error response
func checkErrorStatus(response []byte, expectedStatuses ...string) (bool, error) {
var errorResp ErrorResponse
if err := json.Unmarshal(response, &errorResp); err != nil {
return false, err
}
if slices.Contains(expectedStatuses, errorResp.Detail.Status) {
return true, nil
}
return false, nil
}
================================================
FILE: pkg/analyzer/analyzers/elevenlabs/result_output.json
================================================
{
"AnalyzerType": 6,
"Bindings": [
{
"Resource": {
"Name": "Ahmed",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/User/b9Rou9mHDmTYd8cdWkg2Yk4P2lq1",
"Type": "User",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "user_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Alice",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/Xb7hH8MSUJpSbSDYk0k2",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Aria",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/9BWtsMINqrJLrRacOk9x",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Bill",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/pqHfZKP75CvOlQylNhV4",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Brian",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/nPczCjzI2devNBz1zQrb",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Callum",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/N2lVS1w4EtoT3dr4eOWO",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Charlie",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/IKne3meq5aSn9XLyUdCD",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Charlotte",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/XB0fDUnXU5powFXDhCwa",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Chris",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/iP95p4xoKVk53GoZ742B",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Daniel",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/onwK4e9ZLuTAKqWW03F9",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Eleven English v1",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_monolingual_v1",
"Type": "Model",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "models_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Eleven English v2",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_english_sts_v2",
"Type": "Model",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "models_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Eleven Flash v2",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_flash_v2",
"Type": "Model",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "models_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Eleven Flash v2.5",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_flash_v2_5",
"Type": "Model",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "models_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Eleven Multilingual v1",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_multilingual_v1",
"Type": "Model",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "models_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Eleven Multilingual v2",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_multilingual_v2",
"Type": "Model",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "models_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Eleven Multilingual v2",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_multilingual_sts_v2",
"Type": "Model",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "models_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Eleven Turbo v2",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_turbo_v2",
"Type": "Model",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "models_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Eleven Turbo v2.5",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_turbo_v2_5",
"Type": "Model",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "models_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Eric",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/cjVigY5qzO86Huf0OWal",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "George",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/JBFqnCBsd6RMkjVDRZzb",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Jessica",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/cgSgspJ2msm6clMCkdW9",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Laura",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/FGY2WhTYpPnrIDTdsKH5",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Liam",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/TX3LPaxmHKxFdv7VOQHJ",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Lily",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/pFZP5JQG7iQjIQuC4Bku",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Matilda",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/XrExE9yKIg1WjnnlVkGX",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "River",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/SAz9YHcvj6GT2YYXdXww",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Roger",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/CwhRBWXzGAHq8TQ4Fs17",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Sarah",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/EXAVITQu4vr4xnSDxMaL",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
},
{
"Resource": {
"Name": "Will",
"FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/bIHbv24MWmeRgasZH58o",
"Type": "Voice",
"Metadata": {
"category": "premade"
},
"Parent": null
},
"Permission": {
"Value": "voices_read",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": {
"Valid_Key": true
}
}
================================================
FILE: pkg/analyzer/analyzers/fastly/fastly.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go fastly
package fastly
import (
"fmt"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeFastly
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, exist := credInfo["key"]
if !exist {
return nil, fmt.Errorf("key not found in credential info")
}
// analyze permissions
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
// secret info to analyzer
return secretInfoToAnalyzerResult(info), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
// just print the error in cli and continue as a partial success
color.Red("[x] Error : %s", err.Error())
}
if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}
color.Green("[!] Valid Fastly API key\n\n")
if info.TokenInfo.hasGlobalScope() {
printUserInfo(info.UserInfo)
}
printScopes(info.TokenInfo.Scopes)
if len(info.Resources) > 0 {
printResources(info.Resources)
}
color.Yellow("\n[i] Expires: %s", info.TokenInfo.ExpiresAt)
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
// create http client
client := analyzers.NewAnalyzeClient(cfg)
var secretInfo = &SecretInfo{}
// capture the token details
if err := captureTokenInfo(client, key, secretInfo); err != nil {
return nil, err
}
/*
Fastly defines four types of permissions. Two of these are related specifically to purging:
- If a token has either `purge_select` or `purge_all` access, it is limited to calling purge-related APIs only.
- If a token has `global` or `global:read` access, it can call APIs that retrieve resource and user information.
*/
if !secretInfo.TokenInfo.hasGlobalScope() {
return secretInfo, nil
}
// capture the user information
if err := captureUserInfo(client, key, secretInfo); err != nil {
return nil, err
}
// capture the resources
if err := captureResources(client, key, secretInfo); err != nil {
// return secretInfo as well in case of error for partial success
return secretInfo, err
}
return secretInfo, nil
}
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeFastly,
Metadata: map[string]any{},
Bindings: make([]analyzers.Binding, 0),
}
// extract information from resource to create bindings and append to result bindings
for _, resource := range info.Resources {
binding := analyzers.Binding{
Resource: *secretInfoResourceToAnalyzerResource(resource),
Permission: analyzers.Permission{
Value: info.TokenInfo.Scope,
},
}
if resource.Parent != nil {
binding.Resource.Parent = secretInfoResourceToAnalyzerResource(*resource.Parent)
}
result.Bindings = append(result.Bindings, binding)
}
return &result
}
// secretInfoResourceToAnalyzerResource translate secret info resource to analyzer resource for binding
func secretInfoResourceToAnalyzerResource(resource FastlyResource) *analyzers.Resource {
analyzerRes := analyzers.Resource{
// make fully qualified name unique
FullyQualifiedName: resource.Type + "/" + resource.ID,
Name: resource.Name,
Type: resource.Type,
Metadata: map[string]any{},
}
for key, value := range resource.Metadata {
analyzerRes.Metadata[key] = value
}
return &analyzerRes
}
// cli print functions
func printUserInfo(user User) {
color.Yellow("[i] User Information:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"ID", "Name", "Login", "Role", "Last Active At"})
t.AppendRow(table.Row{color.GreenString(user.ID), color.GreenString(user.Name), color.GreenString(user.Login), color.GreenString(user.Role), color.GreenString(user.LastActiveAt)})
t.Render()
}
func printScopes(scopes []string) {
color.Yellow("[i] Scopes:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scopes"})
for _, scope := range scopes {
t.AppendRow(table.Row{color.GreenString(scope)})
}
t.Render()
}
func printResources(resources []FastlyResource) {
color.Yellow("[i] Resources:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Name", "Type"})
for _, resource := range resources {
t.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/fastly/fastly_test.go
================================================
package fastly
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed result_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
key := testSecrets.MustGetField("FASTLYPERSONALTOKEN_TOKEN")
tests := []struct {
name string
key string
want []byte // JSON string
wantErr bool
}{
{
name: "valid fastly token",
key: key,
want: expectedOutput,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.FullyQualifiedName == bindings[j].Resource.FullyQualifiedName {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.FullyQualifiedName < bindings[j].Resource.FullyQualifiedName
})
}
================================================
FILE: pkg/analyzer/analyzers/fastly/models.go
================================================
package fastly
import "sync"
const (
// types
TypeUserToken string = "User Token"
TypeAutomationToken string = "Automation Token"
TypeService string = "Service"
TypeSvcVersion string = "Service Version"
TypeSvcVersionACL string = "Service Version ACL"
TypeSvcVersionDict string = "Service Version Dictionary"
TypeSvcVersionBackend string = "Service Version Backend"
TypeSvcVersionDomain string = "Service Version Domain"
TypeSvcVersionHealthCheck string = "Service Version Health Check"
TypeConfigStore string = "Config Store"
TypeSecretStore string = "Secret Store"
TypeTLSPrivateKey string = "TLS Private Key"
TypeTLSCertificate string = "TLS Certificates"
TypeTLSDomain string = "TLS Domain"
TypeInvoice string = "Invoice"
)
type SecretInfo struct {
mu sync.RWMutex
UserInfo User
TokenInfo SelfToken
Resources []FastlyResource
}
type FastlyResource struct {
ID string
Name string
Type string
Metadata map[string]string
Parent *FastlyResource
}
// AppendResource append resource to secret info resource list
func (s *SecretInfo) appendResource(resource FastlyResource) {
s.mu.Lock()
defer s.mu.Unlock()
s.Resources = append(s.Resources, resource)
}
// listResourceByType returns a list of resources matching the given type.
func (s *SecretInfo) listResourceByType(resourceType string) []FastlyResource {
s.mu.RLock()
defer s.mu.RUnlock()
resources := make([]FastlyResource, 0, len(s.Resources))
for _, resource := range s.Resources {
if resource.Type == resourceType {
resources = append(resources, resource)
}
}
return resources
}
// API Response models
// User is /current_user API Response
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Login string `json:"login"`
Role string `json:"role"`
LastActiveAt string `json:"last_active_at"`
}
// SelfToken is /tokens/self API Response
type SelfToken struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Name string `json:"name"`
LastUsedAt string `json:"last_used_at"`
ExpiresAt string `json:"expires_at"`
Scope string `json:"scope"`
Scopes []string `json:"scopes"`
Services []string `json:"services"`
}
// hasGlobalScope returns true if any global scope is assigned to the token
func (t SelfToken) hasGlobalScope() bool {
for _, scope := range t.Scopes {
if scope == PermissionStrings[Global] || scope == PermissionStrings[GlobalRead] {
return true
}
}
return false
}
// TokenData is /automation-tokens API Response
type TokenData struct {
Data []Token `json:"data"`
}
// Token is /tokens API Response
type Token struct {
ID string `json:"id"`
Name string `json:"name"`
Scope string `json:"scope"`
Role string `json:"role"`
ExpiresAt string `json:"expires_at"`
}
// Service is /service API Response
type Service struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
// Version is /service//version API Response
type Version struct {
Number int `json:"number"`
Active bool `json:"active"`
Deployed bool `json:"deployed"`
ServiceID string `json:"service_id"`
}
// ACL is /service//version//acl API Response
type ACL struct {
ID string `json:"id"`
Name string `json:"name"`
}
// Dictionary is the /service//version//dictionary API Response
type Dictionary struct {
ID string `json:"id"`
Name string `json:"name"`
}
// Backend is the /service//version//backend API Response
type Backend struct {
Name string `json:"name"`
Address string `json:"address"`
Port string `json:"port"`
}
// Domain is the /service//version//domain API Response
type Domain struct {
Name string `json:"name"`
}
// HealthCheck is the /service//version//healthcheck API Response
type HealthCheck struct {
Name string `json:"name"`
Host string `json:"host"`
Path string `json:"path"`
Method string `json:"method"`
}
// ConfigStore is the /resources/stores/config API Response
type ConfigStore struct {
ID string `json:"id"`
Name string `json:"name"`
}
// SecretStoreData is the /resources/stores/secret API Response
type SecretStoreData struct {
Data []SecretStore `json:"data"`
}
// SecretStore is a single store in SecretStoreData
type SecretStore struct {
ID string `json:"id"`
Name string `json:"name"`
}
// TLSPrivateKeyData is the /tls/private_keys API Response
type TLSPrivateKeyData struct {
Data []TLSPrivateKey `json:"data"`
}
// TLSPrivateKey is the single TLS private key in TLSPrivateKeyData
type TLSPrivateKey struct {
ID string `json:"id"`
Name string `json:"name"`
}
// TLSCertificatesData is the /tls/certificates API Response
type TLSCertificatesData struct {
Data []TLSCertificate `json:"data"`
}
// TLSCertificate is the single TLS certificate in TLSCertificatesData
type TLSCertificate struct {
ID string `json:"id"`
Name string `json:"name"`
}
// TLSDomainsData is the /tls/domains API Response
type TLSDomainsData struct {
Data []TLSDomain `json:"data"`
}
// TLSDomain is the single TLS Domain in TLSDomainsData
type TLSDomain struct {
ID string `json:"id"`
}
// InvoicesData is the /billing/v3/invoices API Response
type InvoicesData struct {
Data []Invoice `json:"data"`
}
// Invoice is the single invoice in InvoicesData
type Invoice struct {
ID string `json:"invoice_id"`
CustomerID string `json:"customer_id"`
Region string `json:"region"`
StatementNo string `json:"statement_number"`
InvoicePostedOn string `json:"invoice_posted_on"`
}
================================================
FILE: pkg/analyzer/analyzers/fastly/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package fastly
import "errors"
type Permission int
const (
Invalid Permission = iota
Global Permission = iota
GlobalRead Permission = iota
PurgeAll Permission = iota
PurgeSelect Permission = iota
)
var (
PermissionStrings = map[Permission]string{
Global: "global",
GlobalRead: "global:read",
PurgeAll: "purge_all",
PurgeSelect: "purge_select",
}
StringToPermission = map[string]Permission{
"global": Global,
"global:read": GlobalRead,
"purge_all": PurgeAll,
"purge_select": PurgeSelect,
}
PermissionIDs = map[Permission]int{
Global: 1,
GlobalRead: 2,
PurgeAll: 3,
PurgeSelect: 4,
}
IdToPermission = map[int]Permission{
1: Global,
2: GlobalRead,
3: PurgeAll,
4: PurgeSelect,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/fastly/permissions.yaml
================================================
permissions:
- global
- global:read
- purge_all
- purge_select
================================================
FILE: pkg/analyzer/analyzers/fastly/requests.go
================================================
package fastly
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"sync"
)
type endpoint int
const (
// list of endpoints
selfToken endpoint = iota
currentUser
userTokens
automationTokens
service
serviceVersions
serviceVersionACLs
serviceVersionDictionaries
serviceVersionBackends
serviceVersionDomains
serviceVersionHealthChecks
configStores
secretStores
tlsPrivateKeys
tlsCertificates
tlsDomains
invoices
)
var (
baseURL = "https://api.fastly.com"
// endpoints contain Fastly API endpoints
endpoints = map[endpoint]string{
selfToken: "/tokens/self",
currentUser: "/current_user",
userTokens: "/tokens",
automationTokens: "/automation-tokens",
service: "/service",
serviceVersions: "/service/%s/version", // require service id
serviceVersionACLs: "/service/%s/version/%s/acl", // require service id and version number
serviceVersionDictionaries: "/service/%s/version/%s/dictionary", // require service id and version number
serviceVersionBackends: "/service/%s/version/%s/backend", // require service id and version number
serviceVersionDomains: "/service/%s/version/%s/domain", // require service id and version number
serviceVersionHealthChecks: "/service/%s/version/%s/healthcheck", // require service id and version number
configStores: "/resources/stores/config",
secretStores: "/resources/stores/secret",
tlsPrivateKeys: "/tls/private_keys",
tlsCertificates: "/tls/certificates",
tlsDomains: "/tls/domains",
invoices: "/billing/v3/invoices",
/*
API:
- /service/service_id/version/version_id/package (The use of this API is discouraged as per documentation due to limited availability release)
- /tls/bulk/certificates (The use of this API is discouraged as per documentation due to limited availability release)
- /security/workspaces (This Fastly Security API is only available to customers with access to the Next-Gen WAF product )
- /events (This API just returns the account events like user logged in or user logged out etc)
Utilities API Docs:
Some of these APIs are deprecated while others return same response for everyone with a global access key.
- https://www.fastly.com/documentation/reference/api/utils/
*/
}
)
// makeFastlyRequest send the API request to passed url with passed key as API Key and return response body and status code
func makeFastlyRequest(client *http.Client, endpoint, key string) ([]byte, int, error) {
// create request
req, err := http.NewRequest(http.MethodGet, baseURL+endpoint, http.NoBody)
if err != nil {
return nil, 0, err
}
// add key in the header
req.Header.Add("Fastly-Key", key)
resp, err := client.Do(req)
if err != nil {
return nil, 0, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
responseBodyByte, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, err
}
return responseBodyByte, resp.StatusCode, nil
}
// captureResources try to capture all the resource that the key can access
func captureResources(client *http.Client, key string, secretInfo *SecretInfo) error {
var (
wg sync.WaitGroup
errAggWg sync.WaitGroup
aggregatedErrs = make([]error, 0)
errChan = make(chan error, 1)
)
errAggWg.Add(1)
go func() {
defer errAggWg.Done()
for err := range errChan {
aggregatedErrs = append(aggregatedErrs, err)
}
}()
// helper to launch tasks concurrently.
launchTask := func(task func() error) {
wg.Add(1)
go func() {
defer wg.Done()
if err := task(); err != nil {
errChan <- err
}
}()
}
launchTask(func() error { return captureAutomationTokens(client, key, secretInfo) })
launchTask(func() error { return captureUserTokens(client, key, secretInfo) })
// capture services and their sub resources
launchTask(func() error {
if err := captureServices(client, key, secretInfo); err != nil {
return err
}
services := secretInfo.listResourceByType(TypeService)
for _, service := range services {
if err := captureSvcVersions(client, key, service, secretInfo); err != nil {
return err
}
}
// capture each version sub resources
versions := secretInfo.listResourceByType(TypeSvcVersion)
for _, version := range versions {
launchTask(func() error { return captureSvcVersionACLs(client, key, version, secretInfo) })
launchTask(func() error { return captureSvcVersionDicts(client, key, version, secretInfo) })
launchTask(func() error { return captureSvcVersionBackends(client, key, version, secretInfo) })
launchTask(func() error { return captureSvcVersionDomains(client, key, version, secretInfo) })
launchTask(func() error { return captureSvcVersionHealthChecks(client, key, version, secretInfo) })
}
return nil
})
launchTask(func() error { return captureConfigStores(client, key, secretInfo) })
launchTask(func() error { return captureSecretStores(client, key, secretInfo) })
launchTask(func() error { return capturePrivateKeys(client, key, secretInfo) })
launchTask(func() error { return captureCertificates(client, key, secretInfo) })
launchTask(func() error { return captureTLSDomains(client, key, secretInfo) })
launchTask(func() error { return captureInvoices(client, key, secretInfo) })
wg.Wait()
close(errChan)
errAggWg.Wait()
if len(aggregatedErrs) > 0 {
return errors.Join(aggregatedErrs...)
}
return nil
}
// captureTokenInfo calls `/tokens/self` API and capture the token information in secretInfo
func captureTokenInfo(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, endpoints[selfToken], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var token SelfToken
if err := json.Unmarshal(respBody, &token); err != nil {
return err
}
if token.ExpiresAt == "" {
token.ExpiresAt = "never"
}
secretInfo.TokenInfo = token
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired api key")
default:
return fmt.Errorf("unexpected status code: %d for API: %s", statusCode, endpoints[selfToken])
}
}
// captureUserInfo calls `/current_user` API and capture the current user information in secretInfo
func captureUserInfo(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, endpoints[currentUser], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var user User
if err := json.Unmarshal(respBody, &user); err != nil {
return err
}
secretInfo.UserInfo = user
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d for API: %s", statusCode, endpoints[currentUser])
}
}
// captureUserTokens calls `/tokens` API
func captureUserTokens(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, endpoints[userTokens], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var tokens []Token
if err := json.Unmarshal(respBody, &tokens); err != nil {
return err
}
for _, token := range tokens {
resource := FastlyResource{
ID: token.ID,
Name: token.Name,
Type: TypeUserToken,
Metadata: map[string]string{
"Scope": token.Scope,
"Role": token.Role,
"Expires At": token.ExpiresAt,
},
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// captureAutomationTokens calls `/automation-tokens` API
func captureAutomationTokens(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, endpoints[automationTokens], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var tokens TokenData
if err := json.Unmarshal(respBody, &tokens); err != nil {
return err
}
for _, token := range tokens.Data {
resource := FastlyResource{
ID: token.ID,
Name: token.Name,
Type: TypeAutomationToken,
Metadata: map[string]string{
"Scope": token.Scope,
"Role": token.Role,
"Expires At": token.ExpiresAt,
},
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// captureServices calls `/service` API
func captureServices(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, endpoints[service], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var services []Service
if err := json.Unmarshal(respBody, &services); err != nil {
return err
}
for _, service := range services {
resource := FastlyResource{
ID: service.ID,
Name: service.Name,
Type: TypeService,
Metadata: map[string]string{
"Service Type": service.Type,
},
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d for API: %s", statusCode, endpoints[service])
}
}
// captureSvcVersions calls `/service//version` API
func captureSvcVersions(client *http.Client, key string, parentService FastlyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, fmt.Sprintf(endpoints[serviceVersions], parentService.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var versions []Version
if err := json.Unmarshal(respBody, &versions); err != nil {
return err
}
for _, version := range versions {
resource := FastlyResource{
ID: strconv.Itoa(version.Number),
Name: parentService.ID + "/version/" + strconv.Itoa(version.Number), // versions has no specific name
Type: TypeSvcVersion,
Metadata: map[string]string{"service_id": version.ServiceID},
Parent: &parentService,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// captureSvcVersionACLs calls `/service//version//acl` API
func captureSvcVersionACLs(client *http.Client, key string, parentVersion FastlyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, fmt.Sprintf(endpoints[serviceVersionACLs], parentVersion.Metadata["service_id"], parentVersion.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var acls []ACL
if err := json.Unmarshal(respBody, &acls); err != nil {
return err
}
for _, acl := range acls {
resource := FastlyResource{
ID: acl.ID,
Name: acl.Name,
Type: TypeSvcVersionACL,
Parent: &parentVersion,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// captureSvcVersionDicts calls `/service//version//dictionaries` API
func captureSvcVersionDicts(client *http.Client, key string, parentVersion FastlyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, fmt.Sprintf(endpoints[serviceVersionDictionaries], parentVersion.Metadata["service_id"], parentVersion.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var dicts []Dictionary
if err := json.Unmarshal(respBody, &dicts); err != nil {
return err
}
for _, dict := range dicts {
resource := FastlyResource{
ID: dict.ID,
Name: dict.Name,
Type: TypeSvcVersionDict,
Parent: &parentVersion,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// captureSvcVersionBackends calls `/service//version//backend` API
func captureSvcVersionBackends(client *http.Client, key string, parentVersion FastlyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, fmt.Sprintf(endpoints[serviceVersionBackends], parentVersion.Metadata["service_id"], parentVersion.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var backends []Backend
if err := json.Unmarshal(respBody, &backends); err != nil {
return err
}
for _, backend := range backends {
resource := FastlyResource{
ID: parentVersion.Metadata["service_id"] + "/version/" + parentVersion.ID + "/backend/" + backend.Name, // no specific ID
Name: backend.Name,
Type: TypeSvcVersionBackend,
Parent: &parentVersion,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// captureSvcVersionDomains calls `/service//version//domain` API
func captureSvcVersionDomains(client *http.Client, key string, parentVersion FastlyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, fmt.Sprintf(endpoints[serviceVersionDomains], parentVersion.Metadata["service_id"], parentVersion.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var domains []Domain
if err := json.Unmarshal(respBody, &domains); err != nil {
return err
}
for _, domain := range domains {
resource := FastlyResource{
ID: parentVersion.Metadata["service_id"] + "/version/" + parentVersion.ID + "/domain/" + domain.Name, // no specific ID
Name: domain.Name,
Type: TypeSvcVersionDomain,
Parent: &parentVersion,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// captureSvcVersionHealthChecks calls `/service//version//healthcheck` API
func captureSvcVersionHealthChecks(client *http.Client, key string, parentVersion FastlyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, fmt.Sprintf(endpoints[serviceVersionHealthChecks], parentVersion.Metadata["service_id"], parentVersion.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var healthChecks []HealthCheck
if err := json.Unmarshal(respBody, &healthChecks); err != nil {
return err
}
for _, healthCheck := range healthChecks {
resource := FastlyResource{
ID: parentVersion.Metadata["service_id"] + "/version/" + parentVersion.ID + "/healthcheck/" + healthCheck.Name, // no specific ID
Name: healthCheck.Name,
Type: TypeSvcVersionHealthCheck,
Parent: &parentVersion,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// captureConfigStores calls `/resources/stores/config` API
func captureConfigStores(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, endpoints[configStores], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var configs []ConfigStore
if err := json.Unmarshal(respBody, &configs); err != nil {
return err
}
for _, config := range configs {
resource := FastlyResource{
ID: config.ID,
Name: config.Name,
Type: TypeConfigStore,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// captureSecretStores calls `/resources/stores/secret` API
func captureSecretStores(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, endpoints[secretStores], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var secretStores SecretStoreData
if err := json.Unmarshal(respBody, &secretStores); err != nil {
return err
}
for _, secret := range secretStores.Data {
resource := FastlyResource{
ID: secret.ID,
Name: secret.Name,
Type: TypeSecretStore,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// capturePrivateKeys calls `/tls/private_keys` API
func capturePrivateKeys(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, endpoints[tlsPrivateKeys], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var privateKeys TLSPrivateKeyData
if err := json.Unmarshal(respBody, &privateKeys); err != nil {
return err
}
for _, privateKey := range privateKeys.Data {
resource := FastlyResource{
ID: privateKey.ID,
Name: privateKey.Name,
Type: TypeTLSPrivateKey,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// captureCertificates calls `/tls/certificates` API
func captureCertificates(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, endpoints[tlsCertificates], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var certData TLSCertificatesData
if err := json.Unmarshal(respBody, &certData); err != nil {
return err
}
for _, cert := range certData.Data {
resource := FastlyResource{
ID: cert.ID,
Name: cert.Name,
Type: TypeTLSCertificate,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// captureTLSDomains calls `/tls/domains` API
func captureTLSDomains(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, endpoints[tlsDomains], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var domainData TLSDomainsData
if err := json.Unmarshal(respBody, &domainData); err != nil {
return err
}
for _, domain := range domainData.Data {
resource := FastlyResource{
ID: domain.ID,
Name: domain.ID,
Type: TypeTLSDomain,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// captureInvoices calls `/billing/v3/invoices` API
func captureInvoices(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeFastlyRequest(client, endpoints[invoices], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var invoices InvoicesData
if err := json.Unmarshal(respBody, &invoices); err != nil {
return err
}
for _, invoice := range invoices.Data {
resource := FastlyResource{
ID: invoice.CustomerID + "/region/" + invoice.Region + "/statement/" + invoice.StatementNo + "/invoice/" + invoice.ID,
Name: invoice.ID, // no specific name
Type: TypeInvoice,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
================================================
FILE: pkg/analyzer/analyzers/fastly/result_output.json
================================================
{
"AnalyzerType": 34,
"Bindings": [
{
"Resource": {
"Name": "test",
"FullyQualifiedName": "Config Store/Q9uDqi7ODnLUrhMFifFVT4",
"Type": "Config Store",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "global:read global",
"Parent": null
}
},
{
"Resource": {
"Name": "centrally-decent-lynx.edgecompute.app",
"FullyQualifiedName": "Service Version Domain/vInh5jJ0qnGdhiCO04INR7/version/1/domain/centrally-decent-lynx.edgecompute.app",
"Type": "Service Version Domain",
"Metadata": {},
"Parent": {
"Name": "vInh5jJ0qnGdhiCO04INR7/version/1",
"FullyQualifiedName": "Service Version/1",
"Type": "Service Version",
"Metadata": {
"service_id": "vInh5jJ0qnGdhiCO04INR7"
},
"Parent": null
}
},
"Permission": {
"Value": "global:read global",
"Parent": null
}
},
{
"Resource": {
"Name": "centrally-decent-lynx.edgecompute.app",
"FullyQualifiedName": "Service Version Domain/vInh5jJ0qnGdhiCO04INR7/version/2/domain/centrally-decent-lynx.edgecompute.app",
"Type": "Service Version Domain",
"Metadata": {},
"Parent": {
"Name": "vInh5jJ0qnGdhiCO04INR7/version/2",
"FullyQualifiedName": "Service Version/2",
"Type": "Service Version",
"Metadata": {
"service_id": "vInh5jJ0qnGdhiCO04INR7"
},
"Parent": null
}
},
"Permission": {
"Value": "global:read global",
"Parent": null
}
},
{
"Resource": {
"Name": "centrally-decent-lynx.edgecompute.app",
"FullyQualifiedName": "Service Version Domain/vInh5jJ0qnGdhiCO04INR7/version/3/domain/centrally-decent-lynx.edgecompute.app",
"Type": "Service Version Domain",
"Metadata": {},
"Parent": {
"Name": "vInh5jJ0qnGdhiCO04INR7/version/3",
"FullyQualifiedName": "Service Version/3",
"Type": "Service Version",
"Metadata": {
"service_id": "vInh5jJ0qnGdhiCO04INR7"
},
"Parent": null
}
},
"Permission": {
"Value": "global:read global",
"Parent": null
}
},
{
"Resource": {
"Name": "Detectors",
"FullyQualifiedName": "Service Version Health Check/vInh5jJ0qnGdhiCO04INR7/version/3/healthcheck/Detectors",
"Type": "Service Version Health Check",
"Metadata": {},
"Parent": {
"Name": "vInh5jJ0qnGdhiCO04INR7/version/3",
"FullyQualifiedName": "Service Version/3",
"Type": "Service Version",
"Metadata": {
"service_id": "vInh5jJ0qnGdhiCO04INR7"
},
"Parent": null
}
},
"Permission": {
"Value": "global:read global",
"Parent": null
}
},
{
"Resource": {
"Name": "yja0K1GNPRDNTA6vizIFK4/version/1",
"FullyQualifiedName": "Service Version/1",
"Type": "Service Version",
"Metadata": {
"service_id": "yja0K1GNPRDNTA6vizIFK4"
},
"Parent": {
"Name": "Truffle Security's website",
"FullyQualifiedName": "Service/yja0K1GNPRDNTA6vizIFK4",
"Type": "Service",
"Metadata": {
"Service Type": "vcl"
},
"Parent": null
}
},
"Permission": {
"Value": "global:read global",
"Parent": null
}
},
{
"Resource": {
"Name": "vInh5jJ0qnGdhiCO04INR7/version/1",
"FullyQualifiedName": "Service Version/1",
"Type": "Service Version",
"Metadata": {
"service_id": "vInh5jJ0qnGdhiCO04INR7"
},
"Parent": {
"Name": "this is a test service",
"FullyQualifiedName": "Service/vInh5jJ0qnGdhiCO04INR7",
"Type": "Service",
"Metadata": {
"Service Type": "wasm"
},
"Parent": null
}
},
"Permission": {
"Value": "global:read global",
"Parent": null
}
},
{
"Resource": {
"Name": "vInh5jJ0qnGdhiCO04INR7/version/2",
"FullyQualifiedName": "Service Version/2",
"Type": "Service Version",
"Metadata": {
"service_id": "vInh5jJ0qnGdhiCO04INR7"
},
"Parent": {
"Name": "this is a test service",
"FullyQualifiedName": "Service/vInh5jJ0qnGdhiCO04INR7",
"Type": "Service",
"Metadata": {
"Service Type": "wasm"
},
"Parent": null
}
},
"Permission": {
"Value": "global:read global",
"Parent": null
}
},
{
"Resource": {
"Name": "vInh5jJ0qnGdhiCO04INR7/version/3",
"FullyQualifiedName": "Service Version/3",
"Type": "Service Version",
"Metadata": {
"service_id": "vInh5jJ0qnGdhiCO04INR7"
},
"Parent": {
"Name": "this is a test service",
"FullyQualifiedName": "Service/vInh5jJ0qnGdhiCO04INR7",
"Type": "Service",
"Metadata": {
"Service Type": "wasm"
},
"Parent": null
}
},
"Permission": {
"Value": "global:read global",
"Parent": null
}
},
{
"Resource": {
"Name": "this is a test service",
"FullyQualifiedName": "Service/vInh5jJ0qnGdhiCO04INR7",
"Type": "Service",
"Metadata": {
"Service Type": "wasm"
},
"Parent": null
},
"Permission": {
"Value": "global:read global",
"Parent": null
}
},
{
"Resource": {
"Name": "Truffle Security's website",
"FullyQualifiedName": "Service/yja0K1GNPRDNTA6vizIFK4",
"Type": "Service",
"Metadata": {
"Service Type": "vcl"
},
"Parent": null
},
"Permission": {
"Value": "global:read global",
"Parent": null
}
},
{
"Resource": {
"Name": "test-user-global",
"FullyQualifiedName": "User Token/24K13teXo9GhmaUGhwBS2V",
"Type": "User Token",
"Metadata": {
"Expires At": "2025-12-31T19:00:00Z",
"Role": "",
"Scope": "global"
},
"Parent": null
},
"Permission": {
"Value": "global:read global",
"Parent": null
}
},
{
"Resource": {
"Name": "test-user-purge-select",
"FullyQualifiedName": "User Token/2782vHUyFqralr1GKmWmVF",
"Type": "User Token",
"Metadata": {
"Expires At": "",
"Role": "",
"Scope": "purge_select"
},
"Parent": null
},
"Permission": {
"Value": "global:read global",
"Parent": null
}
},
{
"Resource": {
"Name": "test",
"FullyQualifiedName": "User Token/278C9jIudzPv9NC6BvZT4z",
"Type": "User Token",
"Metadata": {
"Expires At": "2025-07-22T19:00:00Z",
"Role": "",
"Scope": "global:read global"
},
"Parent": null
},
"Permission": {
"Value": "global:read global",
"Parent": null
}
},
{
"Resource": {
"Name": "integration-test",
"FullyQualifiedName": "User Token/2ICO7ArmhY8OMiiOyNpXfc",
"Type": "User Token",
"Metadata": {
"Expires At": "",
"Role": "",
"Scope": "global:read global"
},
"Parent": null
},
"Permission": {
"Value": "global:read global",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": {}
}
================================================
FILE: pkg/analyzer/analyzers/figma/endpoints.json
================================================
{
"files:read": {
"url": "https://api.figma.com/v1/me",
"method": "GET",
"expected_status_code_with_scope": 200,
"expected_status_code_without_scope": 403
},
"library_analytics:read": {
"url": "https://api.figma.com/v1/analytics/libraries/0/component/actions",
"method": "GET",
"expected_status_code_with_scope": 400,
"expected_status_code_without_scope": 403
},
"file_dev_resources:write": {
"url": "https://api.figma.com/v1/dev_resources",
"method": "POST",
"expected_status_code_with_scope": 400,
"expected_status_code_without_scope": 403
},
"file_variables:read": {
"url": "https://api.figma.com/v1/files/0/variables/published",
"method": "GET",
"expected_status_code_with_scope": 404,
"expected_status_code_without_scope": 403
},
"webhooks:write": {
"url": "https://api.figma.com/v2/webhooks",
"method": "POST",
"expected_status_code_with_scope": 400,
"expected_status_code_without_scope": 403
}
}
================================================
FILE: pkg/analyzer/analyzers/figma/expected_output.json
================================================
{"AnalyzerType":32,"Bindings":[{"Resource":{"Name":"Source Integration","FullyQualifiedName":"1287160752716166666","Type":"user","Metadata":{"email":"source-integrations@trufflesec.com","img_url":"https://www.gravatar.com/avatar/48da7f448c34d4271a51d2ccf058f473?size=240&default=https%3A%2F%2Fs3-alpha.figma.com%2Fstatic%2Fuser_s_v2.png"},"Parent":null},"Permission":{"Value":"files:read","Parent":null}}],"UnboundedResources":null,"Metadata":null}
================================================
FILE: pkg/analyzer/analyzers/figma/figma.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go figma
package figma
import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"regexp"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeFigma }
type ScopeStatus string
const (
StatusError ScopeStatus = "Error"
StatusGranted ScopeStatus = "Granted"
StatusDenied ScopeStatus = "Denied"
StatusUnverified ScopeStatus = "Unverified"
)
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
token, ok := credInfo["token"]
if !ok {
return nil, errors.New("token not found in credInfo")
}
info, err := AnalyzePermissions(a.Cfg, token)
if err != nil {
return nil, err
}
return MapToAnalyzerResult(info), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, token string) {
info, err := AnalyzePermissions(cfg, token)
if err != nil {
color.Red("[x] Error : %s", err.Error())
return
}
color.Green("[!] Valid Figma Personal Access Token\n\n")
PrintUserAndPermissions(info)
}
func AnalyzePermissions(cfg *config.Config, token string) (*secretInfo, error) {
client := analyzers.NewAnalyzeClient(cfg)
allScopes := getAllScopes()
scopeToEndpoints, err := getScopeEndpointsMap()
if err != nil {
return nil, err
}
var info = &secretInfo{Scopes: map[Scope]ScopeStatus{}}
for _, scope := range allScopes {
info.Scopes[scope] = StatusUnverified
}
for _, scope := range orderedScopeList {
endpoint, err := getScopeEndpoint(scopeToEndpoints, scope)
if err != nil {
return nil, err
}
resp, err := callAPIEndpoint(client, token, endpoint)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
scopeStatus := determineScopeStatus(resp.StatusCode, endpoint)
if scopeStatus == StatusGranted {
if scope == ScopeFilesRead {
if err := json.Unmarshal(body, &info.UserInfo); err != nil {
return nil, fmt.Errorf("error decoding user info from response %v", err)
}
}
info.Scopes[scope] = StatusGranted
}
// If the token does NOT have the scope, response will include all the scopes it does have
if scopeStatus == StatusDenied {
scopes, ok := extractScopesFromError(body)
if !ok {
return nil, fmt.Errorf("could not extract scopes from error message")
}
for scope := range info.Scopes {
info.Scopes[scope] = StatusDenied
}
for _, scope := range scopes {
info.Scopes[scope] = StatusGranted
}
// We have enough info to finish analysis
break
}
}
return info, nil
}
// determineScopeStatus takes the API response status code and uses it along with the expected
// status codes to dermine whether the access token has the required scope to perform that action.
// It returns a ScopeStatus which can be Granted, Denied, or Unverified.
func determineScopeStatus(statusCode int, endpoint endpoint) ScopeStatus {
if statusCode == endpoint.ExpectedStatusCodeWithScope || statusCode == http.StatusOK {
return StatusGranted
}
if statusCode == endpoint.ExpectedStatusCodeWithoutScope {
return StatusDenied
}
// Can not determine scope as the expected error is unknown
return StatusUnverified
}
// Matches API response body with expected message pattern in case the token is missing a scope
// If the responses match, we can extract all available scopes from the response msg
func extractScopesFromError(body []byte) ([]Scope, bool) {
filteredBody := filterErrorResponseBody(string(body))
re := regexp.MustCompile(`Invalid scope(?:\(s\))?: ([a-zA-Z_:, ]+)\. This endpoint requires.*`)
matches := re.FindStringSubmatch(filteredBody)
if len(matches) > 1 {
scopes := strings.Split(matches[1], ", ")
return getScopesFromScopeStrings(scopes), true
}
return nil, false
}
// The filterErrorResponseBody function cleans the provided "invalid permission" API
// response message by removing the characters '"', '[', ']', '\', and '"'.
func filterErrorResponseBody(msg string) string {
result := strings.ReplaceAll(msg, "\\", "")
result = strings.ReplaceAll(result, "\"", "")
result = strings.ReplaceAll(result, "[", "")
return strings.ReplaceAll(result, "]", "")
}
func MapToAnalyzerResult(info *secretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeFigma,
}
var permissions []analyzers.Permission
for scope, status := range info.Scopes {
if status != StatusGranted {
continue
}
permissions = append(permissions, analyzers.Permission{Value: string(scope)})
}
userResource := analyzers.Resource{
Name: info.UserInfo.Handle,
FullyQualifiedName: info.UserInfo.ID,
Type: "user",
Metadata: map[string]any{
"email": info.UserInfo.Email,
"img_url": info.UserInfo.ImgURL,
},
}
result.Bindings = analyzers.BindAllPermissions(userResource, permissions...)
return &result
}
func PrintUserAndPermissions(info *secretInfo) {
color.Yellow("[i] User Info:")
t1 := table.NewWriter()
t1.SetOutputMirror(os.Stdout)
t1.AppendHeader(table.Row{"ID", "Handle", "Email", "Image URL"})
t1.AppendRow(table.Row{
color.GreenString(info.UserInfo.ID),
color.GreenString(info.UserInfo.Handle),
color.GreenString(info.UserInfo.Email),
color.GreenString(info.UserInfo.ImgURL),
})
t1.SetOutputMirror(os.Stdout)
t1.Render()
color.Yellow("\n[i] Scopes:")
t2 := table.NewWriter()
t2.AppendHeader(table.Row{"Scope", "Status", "Actions"})
for scope, status := range info.Scopes {
actions := getScopeActions(scope)
rows := []table.Row{}
for i, action := range actions {
var scopeCell string
var statusCell string
if i == 0 {
scopeCell = color.GreenString(string(scope))
statusCell = color.GreenString(string(status))
}
rows = append(rows, table.Row{scopeCell, statusCell, color.GreenString(action)})
}
t2.AppendRows(rows)
t2.AppendSeparator()
}
t2.SetOutputMirror(os.Stdout)
t2.Render()
fmt.Printf("%s: https://www.figma.com/developers/api\n\n", color.GreenString("Ref"))
}
================================================
FILE: pkg/analyzer/analyzers/figma/figma_test.go
================================================
package figma
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(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)
}
tests := []struct {
name string
token string
want string // JSON string
wantErr bool
}{
{
token: testSecrets.MustGetField("FIGMAPERSONALACCESSTOKEN_V2_TOKEN"),
name: "valid Figma Personal Access Token",
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"token": tt.token})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/figma/models.go
================================================
package figma
type userInfo struct {
ID string `json:"id"`
Handle string `json:"handle"`
ImgURL string `json:"img_url"`
Email string `json:"email"`
}
type secretInfo struct {
UserInfo userInfo
Scopes map[Scope]ScopeStatus
}
type endpoint struct {
URL string `json:"url"`
Method string `json:"method"`
ExpectedStatusCodeWithScope int `json:"expected_status_code_with_scope"`
ExpectedStatusCodeWithoutScope int `json:"expected_status_code_without_scope"`
}
================================================
FILE: pkg/analyzer/analyzers/figma/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package figma
import "errors"
type Permission int
const (
Invalid Permission = iota
FilesRead Permission = iota
FileVariablesRead Permission = iota
FileVariablesWrite Permission = iota
FileCommentsWrite Permission = iota
FileDevResourcesRead Permission = iota
FileDevResourcesWrite Permission = iota
LibraryAnalyticsRead Permission = iota
WebhooksWrite Permission = iota
)
var (
PermissionStrings = map[Permission]string{
FilesRead: "files:read",
FileVariablesRead: "file_variables:read",
FileVariablesWrite: "file_variables:write",
FileCommentsWrite: "file_comments:write",
FileDevResourcesRead: "file_dev_resources:read",
FileDevResourcesWrite: "file_dev_resources:write",
LibraryAnalyticsRead: "library_analytics:read",
WebhooksWrite: "webhooks:write",
}
StringToPermission = map[string]Permission{
"files:read": FilesRead,
"file_variables:read": FileVariablesRead,
"file_variables:write": FileVariablesWrite,
"file_comments:write": FileCommentsWrite,
"file_dev_resources:read": FileDevResourcesRead,
"file_dev_resources:write": FileDevResourcesWrite,
"library_analytics:read": LibraryAnalyticsRead,
"webhooks:write": WebhooksWrite,
}
PermissionIDs = map[Permission]int{
FilesRead: 1,
FileVariablesRead: 2,
FileVariablesWrite: 3,
FileCommentsWrite: 4,
FileDevResourcesRead: 5,
FileDevResourcesWrite: 6,
LibraryAnalyticsRead: 7,
WebhooksWrite: 8,
}
IdToPermission = map[int]Permission{
1: FilesRead,
2: FileVariablesRead,
3: FileVariablesWrite,
4: FileCommentsWrite,
5: FileDevResourcesRead,
6: FileDevResourcesWrite,
7: LibraryAnalyticsRead,
8: WebhooksWrite,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/figma/permissions.yaml
================================================
permissions:
- files:read
- file_variables:read
- file_variables:write
- file_comments:write
- file_dev_resources:read
- file_dev_resources:write
- library_analytics:read
- webhooks:write
================================================
FILE: pkg/analyzer/analyzers/figma/requests.go
================================================
package figma
import (
"net/http"
)
func callAPIEndpoint(client *http.Client, token string, endpoint endpoint) (*http.Response, error) {
req, err := http.NewRequest(endpoint.Method, endpoint.URL, nil)
if err != nil {
return nil, err
}
req.Header.Set("X-FIGMA-TOKEN", token)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}
================================================
FILE: pkg/analyzer/analyzers/figma/scopes.go
================================================
package figma
import (
_ "embed"
"encoding/json"
"errors"
)
type Scope string
const (
ScopeFilesRead Scope = "files:read"
ScopeFileVariablesRead Scope = "file_variables:read"
ScopeFileVariablesWrite Scope = "file_variables:write"
ScopeFileCommentsWrite Scope = "file_comments:write"
ScopeFileDevResourcesRead Scope = "file_dev_resources:read"
ScopeFileDevResourcesWrite Scope = "file_dev_resources:write"
ScopeLibraryAnalyticsRead Scope = "library_analytics:read"
ScopeWebhooksWrite Scope = "webhooks:write"
)
// This list orders the scope in which they must be tested
var orderedScopeList = []Scope{
ScopeFilesRead,
ScopeLibraryAnalyticsRead,
ScopeFileDevResourcesWrite,
ScopeFileVariablesRead,
ScopeWebhooksWrite,
}
var scopeToActions = map[Scope][]string{
ScopeFilesRead: {
"Get user info",
"Read files",
"Read projects",
"Read users",
"Read versions",
"Read comments",
"Read components & styles",
"Read webhooks",
},
ScopeFileVariablesRead: {
"Read file variables",
},
ScopeFileVariablesWrite: {
"Write file variables",
},
ScopeFileCommentsWrite: {
"Post comments",
"Delete comments",
"Post comment reactions",
"Delete comment reactions",
},
ScopeFileDevResourcesRead: {
"Read file dev resources",
},
ScopeFileDevResourcesWrite: {
"Write file dev resources",
},
ScopeLibraryAnalyticsRead: {
"Read design system analytics",
},
ScopeWebhooksWrite: {
"Create webhooks",
"Manage webhooks",
},
}
var scopeStringToScope map[string]Scope
//go:embed endpoints.json
var endpointsConfig []byte
func init() {
scopeStringToScope = map[string]Scope{
string(ScopeFilesRead): ScopeFilesRead,
string(ScopeFileVariablesRead): ScopeFileVariablesRead,
string(ScopeFileVariablesWrite): ScopeFileVariablesWrite,
string(ScopeFileCommentsWrite): ScopeFileCommentsWrite,
string(ScopeFileDevResourcesRead): ScopeFileDevResourcesRead,
string(ScopeFileDevResourcesWrite): ScopeFileDevResourcesWrite,
string(ScopeLibraryAnalyticsRead): ScopeLibraryAnalyticsRead,
string(ScopeWebhooksWrite): ScopeWebhooksWrite,
}
}
func getScopeActions(scope Scope) []string {
return scopeToActions[scope]
}
func getScopeEndpointsMap() (map[Scope]endpoint, error) {
var scopeToEndpoints map[Scope]endpoint
if err := json.Unmarshal(endpointsConfig, &scopeToEndpoints); err != nil {
return nil, errors.New("failed to unmarshal endpoints.json: " + err.Error())
}
return scopeToEndpoints, nil
}
func getScopeEndpoint(scopeToEndpoint map[Scope]endpoint, scope Scope) (endpoint, error) {
if endpoint, ok := scopeToEndpoint[scope]; ok {
return endpoint, nil
}
return endpoint{}, errors.New("invalid scope or endpoint doesn't exist")
}
func getScopesFromScopeStrings(scopeStrings []string) []Scope {
var scopes []Scope
for _, scopeString := range scopeStrings {
if scope, ok := scopeStringToScope[scopeString]; ok {
scopes = append(scopes, scope)
}
}
return scopes
}
func getAllScopes() []Scope {
return []Scope{
ScopeFilesRead,
ScopeFileVariablesRead,
ScopeFileVariablesWrite,
ScopeFileCommentsWrite,
ScopeFileDevResourcesRead,
ScopeFileDevResourcesWrite,
ScopeLibraryAnalyticsRead,
ScopeWebhooksWrite,
}
}
================================================
FILE: pkg/analyzer/analyzers/github/classic/classic.yaml
================================================
permissions:
- repo
- repo:status
- repo_deployment
- public_repo
- repo:invite
- security_events
- workflow
- write:packages
- read:packages
- delete:packages
- admin:org
- write:org
- read:org
- manage_runners:org
- admin:public_key
- write:public_key
- read:public_key
- admin:repo_hook
- write:repo_hook
- read:repo_hook
- admin:org_hook
- gist
- notifications
- user
- read:user
- user:email
- user:follow
- delete_repo
- write:discussion
- read:discussion
- admin:enterprise
- manage_runners:enterprise
- manage_billing:enterprise
- read:enterprise
- audit_log
- read:audit_log
- codespace
- codespace:secrets
- copilot
- manage_billing:copilot
- project
- read:project
- admin:gpg_key
- write:gpg_key
- read:gpg_key
- admin:ssh_signing_key
- write:ssh_signing_key
- read:ssh_signing_key
================================================
FILE: pkg/analyzer/analyzers/github/classic/classic_permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package classic
import "errors"
type Permission int
const (
Invalid Permission = iota
Repo Permission = iota
RepoStatus Permission = iota
RepoDeployment Permission = iota
PublicRepo Permission = iota
RepoInvite Permission = iota
SecurityEvents Permission = iota
Workflow Permission = iota
WritePackages Permission = iota
ReadPackages Permission = iota
DeletePackages Permission = iota
AdminOrg Permission = iota
WriteOrg Permission = iota
ReadOrg Permission = iota
ManageRunnersOrg Permission = iota
AdminPublicKey Permission = iota
WritePublicKey Permission = iota
ReadPublicKey Permission = iota
AdminRepoHook Permission = iota
WriteRepoHook Permission = iota
ReadRepoHook Permission = iota
AdminOrgHook Permission = iota
Gist Permission = iota
Notifications Permission = iota
User Permission = iota
ReadUser Permission = iota
UserEmail Permission = iota
UserFollow Permission = iota
DeleteRepo Permission = iota
WriteDiscussion Permission = iota
ReadDiscussion Permission = iota
AdminEnterprise Permission = iota
ManageRunnersEnterprise Permission = iota
ManageBillingEnterprise Permission = iota
ReadEnterprise Permission = iota
AuditLog Permission = iota
ReadAuditLog Permission = iota
Codespace Permission = iota
CodespaceSecrets Permission = iota
Copilot Permission = iota
ManageBillingCopilot Permission = iota
Project Permission = iota
ReadProject Permission = iota
AdminGpgKey Permission = iota
WriteGpgKey Permission = iota
ReadGpgKey Permission = iota
AdminSshSigningKey Permission = iota
WriteSshSigningKey Permission = iota
ReadSshSigningKey Permission = iota
)
var (
PermissionStrings = map[Permission]string{
Repo: "repo",
RepoStatus: "repo:status",
RepoDeployment: "repo_deployment",
PublicRepo: "public_repo",
RepoInvite: "repo:invite",
SecurityEvents: "security_events",
Workflow: "workflow",
WritePackages: "write:packages",
ReadPackages: "read:packages",
DeletePackages: "delete:packages",
AdminOrg: "admin:org",
WriteOrg: "write:org",
ReadOrg: "read:org",
ManageRunnersOrg: "manage_runners:org",
AdminPublicKey: "admin:public_key",
WritePublicKey: "write:public_key",
ReadPublicKey: "read:public_key",
AdminRepoHook: "admin:repo_hook",
WriteRepoHook: "write:repo_hook",
ReadRepoHook: "read:repo_hook",
AdminOrgHook: "admin:org_hook",
Gist: "gist",
Notifications: "notifications",
User: "user",
ReadUser: "read:user",
UserEmail: "user:email",
UserFollow: "user:follow",
DeleteRepo: "delete_repo",
WriteDiscussion: "write:discussion",
ReadDiscussion: "read:discussion",
AdminEnterprise: "admin:enterprise",
ManageRunnersEnterprise: "manage_runners:enterprise",
ManageBillingEnterprise: "manage_billing:enterprise",
ReadEnterprise: "read:enterprise",
AuditLog: "audit_log",
ReadAuditLog: "read:audit_log",
Codespace: "codespace",
CodespaceSecrets: "codespace:secrets",
Copilot: "copilot",
ManageBillingCopilot: "manage_billing:copilot",
Project: "project",
ReadProject: "read:project",
AdminGpgKey: "admin:gpg_key",
WriteGpgKey: "write:gpg_key",
ReadGpgKey: "read:gpg_key",
AdminSshSigningKey: "admin:ssh_signing_key",
WriteSshSigningKey: "write:ssh_signing_key",
ReadSshSigningKey: "read:ssh_signing_key",
}
StringToPermission = map[string]Permission{
"repo": Repo,
"repo:status": RepoStatus,
"repo_deployment": RepoDeployment,
"public_repo": PublicRepo,
"repo:invite": RepoInvite,
"security_events": SecurityEvents,
"workflow": Workflow,
"write:packages": WritePackages,
"read:packages": ReadPackages,
"delete:packages": DeletePackages,
"admin:org": AdminOrg,
"write:org": WriteOrg,
"read:org": ReadOrg,
"manage_runners:org": ManageRunnersOrg,
"admin:public_key": AdminPublicKey,
"write:public_key": WritePublicKey,
"read:public_key": ReadPublicKey,
"admin:repo_hook": AdminRepoHook,
"write:repo_hook": WriteRepoHook,
"read:repo_hook": ReadRepoHook,
"admin:org_hook": AdminOrgHook,
"gist": Gist,
"notifications": Notifications,
"user": User,
"read:user": ReadUser,
"user:email": UserEmail,
"user:follow": UserFollow,
"delete_repo": DeleteRepo,
"write:discussion": WriteDiscussion,
"read:discussion": ReadDiscussion,
"admin:enterprise": AdminEnterprise,
"manage_runners:enterprise": ManageRunnersEnterprise,
"manage_billing:enterprise": ManageBillingEnterprise,
"read:enterprise": ReadEnterprise,
"audit_log": AuditLog,
"read:audit_log": ReadAuditLog,
"codespace": Codespace,
"codespace:secrets": CodespaceSecrets,
"copilot": Copilot,
"manage_billing:copilot": ManageBillingCopilot,
"project": Project,
"read:project": ReadProject,
"admin:gpg_key": AdminGpgKey,
"write:gpg_key": WriteGpgKey,
"read:gpg_key": ReadGpgKey,
"admin:ssh_signing_key": AdminSshSigningKey,
"write:ssh_signing_key": WriteSshSigningKey,
"read:ssh_signing_key": ReadSshSigningKey,
}
PermissionIDs = map[Permission]int{
Repo: 1,
RepoStatus: 2,
RepoDeployment: 3,
PublicRepo: 4,
RepoInvite: 5,
SecurityEvents: 6,
Workflow: 7,
WritePackages: 8,
ReadPackages: 9,
DeletePackages: 10,
AdminOrg: 11,
WriteOrg: 12,
ReadOrg: 13,
ManageRunnersOrg: 14,
AdminPublicKey: 15,
WritePublicKey: 16,
ReadPublicKey: 17,
AdminRepoHook: 18,
WriteRepoHook: 19,
ReadRepoHook: 20,
AdminOrgHook: 21,
Gist: 22,
Notifications: 23,
User: 24,
ReadUser: 25,
UserEmail: 26,
UserFollow: 27,
DeleteRepo: 28,
WriteDiscussion: 29,
ReadDiscussion: 30,
AdminEnterprise: 31,
ManageRunnersEnterprise: 32,
ManageBillingEnterprise: 33,
ReadEnterprise: 34,
AuditLog: 35,
ReadAuditLog: 36,
Codespace: 37,
CodespaceSecrets: 38,
Copilot: 39,
ManageBillingCopilot: 40,
Project: 41,
ReadProject: 42,
AdminGpgKey: 43,
WriteGpgKey: 44,
ReadGpgKey: 45,
AdminSshSigningKey: 46,
WriteSshSigningKey: 47,
ReadSshSigningKey: 48,
}
IdToPermission = map[int]Permission{
1: Repo,
2: RepoStatus,
3: RepoDeployment,
4: PublicRepo,
5: RepoInvite,
6: SecurityEvents,
7: Workflow,
8: WritePackages,
9: ReadPackages,
10: DeletePackages,
11: AdminOrg,
12: WriteOrg,
13: ReadOrg,
14: ManageRunnersOrg,
15: AdminPublicKey,
16: WritePublicKey,
17: ReadPublicKey,
18: AdminRepoHook,
19: WriteRepoHook,
20: ReadRepoHook,
21: AdminOrgHook,
22: Gist,
23: Notifications,
24: User,
25: ReadUser,
26: UserEmail,
27: UserFollow,
28: DeleteRepo,
29: WriteDiscussion,
30: ReadDiscussion,
31: AdminEnterprise,
32: ManageRunnersEnterprise,
33: ManageBillingEnterprise,
34: ReadEnterprise,
35: AuditLog,
36: ReadAuditLog,
37: Codespace,
38: CodespaceSecrets,
39: Copilot,
40: ManageBillingCopilot,
41: Project,
42: ReadProject,
43: AdminGpgKey,
44: WriteGpgKey,
45: ReadGpgKey,
46: AdminSshSigningKey,
47: WriteSshSigningKey,
48: ReadSshSigningKey,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/github/classic/classictoken.go
================================================
//go:generate generate_permissions classic.yaml classic_permissions.go classic
package classic
import (
"fmt"
"os"
"strings"
"github.com/fatih/color"
gh "github.com/google/go-github/v67/github"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
var SCOPE_ORDER = [][]Permission{
{Repo, RepoStatus, RepoDeployment, PublicRepo, RepoInvite, SecurityEvents},
{Workflow},
{WritePackages, ReadPackages},
{DeletePackages},
{AdminOrg, WriteOrg, ReadOrg, ManageRunnersOrg},
{AdminPublicKey, WritePublicKey, ReadPublicKey},
{AdminRepoHook, WriteRepoHook, ReadRepoHook},
{AdminOrgHook},
{Gist},
{Notifications},
{User, ReadUser, UserEmail, UserFollow},
{DeleteRepo},
{WriteDiscussion, ReadDiscussion},
{AdminEnterprise, ManageRunnersEnterprise, ManageBillingEnterprise, ReadEnterprise},
{AuditLog, ReadAuditLog},
{Codespace, CodespaceSecrets},
{Copilot, ManageBillingCopilot},
{Project, ReadProject},
{AdminGpgKey, WriteGpgKey, ReadGpgKey},
{AdminSshSigningKey, WriteSshSigningKey, ReadSshSigningKey},
}
var SCOPE_TO_SUB_SCOPE = map[Permission][]Permission{
Repo: {RepoStatus, RepoDeployment, PublicRepo, RepoInvite, SecurityEvents},
WritePackages: {ReadPackages},
AdminOrg: {WriteOrg, ReadOrg, ManageRunnersOrg},
WriteOrg: {ReadOrg},
AdminPublicKey: {WritePublicKey, ReadPublicKey},
WritePublicKey: {ReadPublicKey},
AdminRepoHook: {WriteRepoHook, ReadRepoHook},
WriteRepoHook: {ReadRepoHook},
User: {ReadUser, UserEmail, UserFollow},
WriteDiscussion: {ReadDiscussion},
AdminEnterprise: {ManageRunnersEnterprise, ManageBillingEnterprise, ReadEnterprise},
ManageBillingEnterprise: {ReadEnterprise},
AuditLog: {ReadAuditLog},
Codespace: {CodespaceSecrets},
Copilot: {ManageBillingCopilot},
Project: {ReadProject},
AdminGpgKey: {WriteGpgKey, ReadGpgKey},
WriteGpgKey: {ReadGpgKey},
AdminSshSigningKey: {WriteSshSigningKey, ReadSshSigningKey},
WriteSshSigningKey: {ReadSshSigningKey},
}
func hasPrivateRepoAccess(scopes map[Permission]bool) bool {
return scopes[Repo]
}
func processScopes(headerScopesSlice []analyzers.Permission) map[Permission]bool {
allScopes := make(map[Permission]bool)
for _, scope := range headerScopesSlice {
allScopes[StringToPermission[scope.Value]] = true
}
for scope := range allScopes {
if subScopes, ok := SCOPE_TO_SUB_SCOPE[scope]; ok {
for _, subScope := range subScopes {
allScopes[subScope] = true
}
}
}
return allScopes
}
func AnalyzeClassicToken(client *gh.Client, meta *common.TokenMetadata) (*common.SecretInfo, error) {
// Convert OauthScopes to have hierarchical permissions.
meta.OauthScopes = oauthScopesToPermissions(meta.OauthScopes...)
scopes := processScopes(meta.OauthScopes)
var repos []*gh.Repository
if hasPrivateRepoAccess(scopes) {
var err error
repos, err = common.GetAllReposForUser(client)
if err != nil {
return nil, err
}
}
gists, err := common.GetAllGistsForUser(client)
if err != nil {
return nil, err
}
return &common.SecretInfo{
Metadata: meta,
Repos: repos,
Gists: gists,
}, nil
}
func filterPrivateRepoScopes(scopes map[Permission]bool) []Permission {
var intersection []Permission
privateScopes := []Permission{Repo, RepoStatus, RepoDeployment, RepoInvite, SecurityEvents, AdminRepoHook, WriteRepoHook, ReadRepoHook}
for _, privScope := range privateScopes {
if scopes[privScope] {
intersection = append(intersection, privScope)
}
}
return intersection
}
func PrintClassicToken(cfg *config.Config, info *common.SecretInfo) {
scopes := processScopes(info.Metadata.OauthScopes)
if len(scopes) == 0 {
color.Red("[x] Classic Token has no scopes")
} else {
printClassicGHPermissions(scopes, cfg.ShowAll)
}
privateScopes := filterPrivateRepoScopes(scopes)
if hasPrivateRepoAccess(scopes) {
color.Green("[!] Token has scope(s) for both public and private repositories. Here's a list of all accessible repositories:")
common.PrintGitHubRepos(info.Repos)
} else if len(privateScopes) > 0 {
color.Yellow("[!] Token has scope(s) useful for accessing both public and private repositories.\n However, without the `repo` scope, we cannot enumerate or access code from private repos.\n Review the permissions associated with the following scopes for more details: %v", joinPermissions(privateScopes))
} else if scopes[PublicRepo] {
color.Yellow("[i] Token is scoped to only public repositories. See https://github.com/%v?tab=repositories", *info.Metadata.User.Login)
} else {
color.Red("[x] Token does not appear scoped to any specific repositories.")
}
common.PrintGists(info.Gists, cfg.ShowAll)
}
func joinPermissions(perms []Permission) string {
var permStrings []string
for _, perm := range perms {
permStr, err := perm.ToString()
if err != nil {
panic(err)
}
permStrings = append(permStrings, permStr)
}
return strings.Join(permStrings, ", ")
}
func scopeFormatter(scope Permission, checked bool, indentation int) (string, string) {
scopeStr, err := scope.ToString()
if err != nil {
panic(err)
}
if indentation != 0 {
scopeStr = strings.Repeat(" ", indentation) + scopeStr
}
if checked {
return color.GreenString(scopeStr), color.GreenString("true")
}
return scopeStr, "false"
}
func printClassicGHPermissions(scopes map[Permission]bool, showAll bool) {
scopeCount := 0
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "In-Scope"})
filteredScopes := make([][]Permission, 0)
for _, scopeSlice := range SCOPE_ORDER {
for _, scope := range scopeSlice {
if scopes[scope] {
filteredScopes = append(filteredScopes, scopeSlice)
break
}
}
}
var formattedScope, status string
var indentation int
if !showAll {
for _, scopeSlice := range filteredScopes {
for ind, scope := range scopeSlice {
if ind == 0 {
indentation = 0
if scopes[scope] {
scopeCount++
formattedScope, status = scopeFormatter(scope, true, indentation)
t.AppendRow([]any{formattedScope, status})
} else {
scopeStr, err := scope.ToString()
if err != nil {
panic(err)
}
t.AppendRow([]any{scopeStr, "----"})
}
} else {
indentation = 2
if scopes[scope] {
scopeCount++
formattedScope, status = scopeFormatter(scope, true, indentation)
t.AppendRow([]any{formattedScope, status})
}
}
}
t.AppendSeparator()
}
} else {
for _, scopeSlice := range SCOPE_ORDER {
for ind, scope := range scopeSlice {
if ind == 0 {
indentation = 0
} else {
indentation = 2
}
if scopes[scope] {
scopeCount++
formattedScope, status = scopeFormatter(scope, true, indentation)
t.AppendRow([]any{formattedScope, status})
} else {
formattedScope, status = scopeFormatter(scope, false, indentation)
t.AppendRow([]any{formattedScope, status})
}
}
t.AppendSeparator()
}
}
if scopeCount == 0 && !showAll {
color.Red("No Scopes Found for the GitHub Token above\n\n")
return
} else if scopeCount == 0 {
color.Red("Found No Scopes for the GitHub Token above\n")
} else {
color.Green(fmt.Sprintf("[!] Found %v Scope(s) for the GitHub Token above\n", scopeCount))
}
t.Render()
fmt.Print("\n\n")
}
// oauthScopesToPermissions takes a list of scopes and returns a slice of
// permissions for it. If the scope has implied permissions, they are included
// as children of the parent scope, and both the parent and children are
// returned in the slice.
func oauthScopesToPermissions(scopes ...analyzers.Permission) []analyzers.Permission {
allPermissions := make([]analyzers.Permission, 0, len(scopes))
for _, scope := range scopes {
allPermissions = append(allPermissions, oauthScopeToPermissions(scope.Value)...)
}
return allPermissions
}
// oauthScopeToPermissions takes a given scope and returns a slice of
// permissions for it. If the scope has implied permissions, they are included
// as children of the parent scope, and both the parent and children are
// returned in the slice.
func oauthScopeToPermissions(scope string) []analyzers.Permission {
parent := analyzers.Permission{Value: scope}
perms := []analyzers.Permission{parent}
subScopes, ok := func() ([]Permission, bool) {
id, err := PermissionFromString(scope)
if err != nil {
return nil, false
}
subScopes, ok := SCOPE_TO_SUB_SCOPE[id]
return subScopes, ok
}()
if !ok {
// No sub-scopes, so the only permission is itself.
return perms
}
// Add all the children to the list of permissions.
for _, subScope := range subScopes {
subScope, _ := subScope.ToString()
perms = append(perms, analyzers.Permission{
Value: subScope,
Parent: &parent,
})
}
return perms
}
================================================
FILE: pkg/analyzer/analyzers/github/common/github.go
================================================
package common
import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/fatih/color"
gh "github.com/google/go-github/v67/github"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
)
type TokenType string
const (
TokenTypeFineGrainedPAT TokenType = "Fine-Grained GitHub Personal Access Token"
TokenTypeClassicPAT TokenType = "Classic GitHub Personal Access Token"
TokenTypeUserToServer TokenType = "GitHub User-to-Server Token"
TokenTypeGitHubToken TokenType = "GitHub Token"
)
func checkFineGrained(token string, oauthScopes []analyzers.Permission) (TokenType, bool) {
// For details on token prefixes, see:
// https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/
// Special case for ghu_ prefix tokens (ex: in a codespace) that don't have the X-OAuth-Scopes header
if strings.HasPrefix(token, "ghu_") {
return TokenTypeUserToServer, true
}
// Handle github_pat_ tokens
if strings.HasPrefix(token, "github_pat") {
return TokenTypeFineGrainedPAT, true
}
// Handle classic PATs
if strings.HasPrefix(token, "ghp_") {
return TokenTypeClassicPAT, false
}
// Catch-all for any other types
// If resp.Header "X-OAuth-Scopes" doesn't exist, then we have fine-grained permissions
if len(oauthScopes) > 0 {
return TokenTypeGitHubToken, false
}
return TokenTypeGitHubToken, true
}
type Permission int
type SecretInfo struct {
Metadata *TokenMetadata
Repos []*gh.Repository
Gists []*gh.Gist
// AccessibleRepos, RepoAccessMap, and UserAccessMap are only set if
// the token has fine-grained access.
AccessibleRepos []*gh.Repository
RepoAccessMap any
UserAccessMap any
}
type TokenMetadata struct {
Type TokenType
FineGrained bool
User *gh.User
Expiration time.Time
// OauthScopes is only set for classic tokens.
OauthScopes []analyzers.Permission
}
// GetTokenMetadata gets the username, expiration date, and x-oauth-scopes headers for a given token
// by sending a GET request to the /user endpoint
// Returns a response object for usage in the checkFineGrained function
func GetTokenMetadata(token string, client *gh.Client) (*TokenMetadata, error) {
user, resp, err := client.Users.Get(context.Background(), "")
if err != nil {
return nil, err
}
var oauthScopes []analyzers.Permission
for _, scope := range resp.Header.Values("X-OAuth-Scopes") {
for _, scope := range strings.Split(scope, ", ") {
oauthScopes = append(oauthScopes, analyzers.Permission{Value: scope})
}
}
tokenType, fineGrained := checkFineGrained(token, oauthScopes)
var expiration time.Time
if tokenType == TokenTypeClassicPAT {
// for classic tokens, github return token expiration time in header in UTC format.
expiration, _ = time.Parse("2006-01-02 15:04:05 UTC", resp.Header.Get("github-authentication-token-expiration"))
} else {
expiration, _ = time.Parse("2006-01-02 15:04:05 -0700", resp.Header.Get("github-authentication-token-expiration"))
}
return &TokenMetadata{
Type: tokenType,
FineGrained: fineGrained,
User: user,
Expiration: expiration,
OauthScopes: oauthScopes,
}, nil
}
func GetAllGistsForUser(client *gh.Client) ([]*gh.Gist, error) {
opt := &gh.GistListOptions{ListOptions: gh.ListOptions{PerPage: 100}}
var allGists []*gh.Gist
page := 1
for {
opt.Page = page
gists, resp, err := client.Gists.List(context.Background(), "", opt)
if err != nil {
color.Red("Error getting gists.")
return nil, err
}
allGists = append(allGists, gists...)
linkHeader := resp.Header.Get("link")
if linkHeader == "" || !strings.Contains(linkHeader, `rel="next"`) {
break
}
page++
}
return allGists, nil
}
func GetAllReposForUser(client *gh.Client) ([]*gh.Repository, error) {
opt := &gh.RepositoryListByAuthenticatedUserOptions{ListOptions: gh.ListOptions{PerPage: 100}}
var allRepos []*gh.Repository
page := 1
for {
opt.Page = page
repos, resp, err := client.Repositories.ListByAuthenticatedUser(context.Background(), opt)
if err != nil {
color.Red("Error getting repos.")
return nil, err
}
allRepos = append(allRepos, repos...)
linkHeader := resp.Header.Get("link")
if linkHeader == "" || !strings.Contains(linkHeader, `rel="next"`) {
break
}
page++
}
return allRepos, nil
}
func PrintGitHubRepos(repos []*gh.Repository) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Repo Name", "Owner", "Repo Link", "Private"})
for _, repo := range repos {
if *repo.Private {
green := color.New(color.FgGreen).SprintFunc()
t.AppendRow([]interface{}{green(*repo.Name), green(*repo.Owner.Login), green(*repo.HTMLURL), green("true")})
} else {
t.AppendRow([]interface{}{*repo.Name, *repo.Owner.Login, *repo.HTMLURL, *repo.Private})
}
}
t.Render()
fmt.Print("\n\n")
}
func PrintGists(gists []*gh.Gist, showAll bool) {
privateCount := 0
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Gist ID", "Gist Link", "Description", "Private"})
for _, gist := range gists {
if gist == nil {
continue
}
gistID := gist.GetID()
gistLink := gist.GetHTMLURL()
gistDescription := gist.GetDescription()
isPublic := gist.GetPublic()
if showAll && isPublic {
t.AppendRow([]any{gistID, gistLink, gistDescription, "false"})
} else if !isPublic {
privateCount++
green := color.New(color.FgGreen).SprintFunc()
t.AppendRow([]any{green(gistID), green(gistLink), green(gistDescription), green("true")})
}
}
if showAll && len(gists) == 0 {
color.Red("[i] No Gist(s) Found\n")
} else if showAll {
color.Yellow("[i] Found %v Total Gist(s) (%v private)\n", len(gists), privateCount)
t.Render()
} else if privateCount == 0 {
color.Red("[i] No Private Gist(s) Found\n")
} else {
color.Green(fmt.Sprintf("[!] Found %v Private Gist(s)\n", privateCount))
t.Render()
}
fmt.Print("\n\n")
}
================================================
FILE: pkg/analyzer/analyzers/github/finegrained/finegrained.go
================================================
//go:generate generate_permissions finegrained.yaml finegrained_permissions.go finegrained
package finegrained
import (
"context"
"errors"
"fmt"
"log"
"os"
"strings"
"github.com/fatih/color"
gh "github.com/google/go-github/v67/github"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
const (
// Random values for testing
RANDOM_STRING = "FQ2pR.4voZg-gJfsqYKx_eLDNF_6BYhw8RL__"
RANDOM_USERNAME = "d" + "ummy" + "acco" + "untgh" + "2024"
RANDOM_REPO = "te" + "st"
RANDOM_INTEGER = 4294967289
)
var ErrInvalid = errors.New("invalid")
var repoPermFuncMap = []func(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error){
getActionsPermission,
getAdministrationPermission,
getCodeScanningAlertsPermission,
getCodespacesPermission,
notImplementedRepoPerm, // ToDo: Implement. Docs make this look org-wide...not repo-based?
getCodespacesMetadataPermission,
getCodespacesSecretsPermission,
getCommitStatusesPermission,
getContentsPermission,
notImplementedRepoPerm, // ToDo: Only supports orgs. Implement once have an org token.
getDependabotAlertsPermission,
getDependabotSecretsPermission,
getDeploymentsPermission,
getEnvironmentsPermission,
getIssuesPermission,
notImplementedRepoPerm, // Skipped until API better documented
getMetadataPermission,
getPagesPermission,
getPullRequestsPermission,
getRepoSecurityPermission,
getSecretScanningPermission,
getSecretsPermission,
getVariablesPermission,
getWebhooksPermission,
notImplementedRepoPerm, // ToDo: Skipped b/c would require us to create a release (High Risk function)
}
var acctPermFuncMap = []func(client *gh.Client, user *gh.User) (Permission, error){
getBlockUserPermission,
getCodespacesUserPermission,
getEmailPermission,
getFollowersPermission,
getGPGKeysPermission,
getGistsPermission,
getGitKeysPermission,
getLimitsPermission,
getPlanPermission,
notImplementedAcctPerm, // Skipped until API better documented
getProfilePermission,
getSigningKeysPermission,
getStarringPermission,
getWatchingPermission,
}
// Define your custom formatter function
func permissionFormatter(key, val any) (string, string) {
if perm, ok := val.(Permission); ok {
permStr, err := perm.ToString()
if err != nil {
log.Fatal(fmt.Errorf("Error converting permission to string: %v", err))
}
var permissionStr string
switch {
case strings.Contains(permStr, "read"):
permissionStr = "READ_ONLY"
case strings.Contains(permStr, "write"):
permissionStr = "READ_WRITE"
default:
permissionStr = "UNKNOWN"
}
switch permissionStr {
case "READ_ONLY":
yellow := color.New(color.FgYellow).SprintFunc()
return yellow(key), yellow(permissionStr)
case "READ_WRITE":
red := color.New(color.FgGreen).SprintFunc()
return red(key), red(permissionStr)
case "UNKNOWN":
blue := color.New(color.FgBlue).SprintFunc()
return blue(key), blue(permissionStr)
}
}
return fmt.Sprintf("%v", key), fmt.Sprintf("%v", val)
}
func notImplementedRepoPerm(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
return NoAccess, nil
}
// notImplementedAcctPerm is a placeholder function that returns a "NOT_IMPLEMENTED" status when a GitHub account permission is not yet implemented.
func notImplementedAcctPerm(client *gh.Client, user *gh.User) (Permission, error) {
return NoAccess, nil
}
func getMetadataPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
// Risk: Extremely Low
// -> GET request to /repos/{owner}/{repo}/collaborators
_, resp, err := client.Repositories.ListCollaborators(context.Background(), *repo.Owner.Login, *repo.Name, nil)
if err != nil {
if resp.StatusCode == 403 {
return NoAccess, nil
}
return Invalid, err
}
// If no error, then we have read access
return MetadataRead, nil
}
func getActionsPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
if *repo.Private {
// Risk: Extremely Low
// -> GET request to /repos/{owner}/{repo}/actions/artifacts
_, resp, err := client.Actions.ListArtifacts(context.Background(), *repo.Owner.Login, *repo.Name, nil)
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Very, very low.
// -> Unless the user has a workflow file named (see RANDOM_STRING above), this will always return 404 for users with READ_WRITE permissions.
// -> POST request to /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches
resp, err = client.Actions.CreateWorkflowDispatchEventByFileName(context.Background(), *repo.Owner.Login, *repo.Name, RANDOM_STRING, gh.CreateWorkflowDispatchEventRequest{})
switch resp.StatusCode {
case 403:
return ActionsRead, nil
case 404:
return ActionsWrite, nil
case 200:
log.Fatal("This shouldn't print. We are enabling a workflow based on a random string " + RANDOM_STRING + ", which most likely doesn't exist.")
return ActionsWrite, nil
default:
return Invalid, err
}
} else {
// Will only land here if already tested one public repo and got a 403.
if currentAccess == NoAccess {
return NoAccess, nil
}
// Risk: Very, very low.
// -> Unless the user has a workflow file named (see RANDOM_STRING above), this will always return 404 for users with READ_WRITE permissions.
// -> POST request to /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches
resp, err := client.Actions.CreateWorkflowDispatchEventByFileName(context.Background(), *repo.Owner.Login, *repo.Name, RANDOM_STRING, gh.CreateWorkflowDispatchEventRequest{})
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 404:
return ActionsWrite, nil
case 200:
log.Fatal("This shouldn't print. We are enabling a workflow based on a random string " + RANDOM_STRING + ", which most likely doesn't exist.")
return ActionsWrite, nil
default:
return Invalid, err
}
}
}
// Continue with the other functions using the same pattern...
func getAdministrationPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
// Risk: Extremely Low
// -> GET request to /repos/{owner}/{repo}/actions/permissions
_, resp, err := client.Repositories.GetActionsPermissions(context.Background(), *repo.Owner.Login, *repo.Name)
switch resp.StatusCode {
case 403, 404:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Extremely Low
// -> GET request to /repos/{owner}/{repo}/rulesets/rule-suites
req, err := client.NewRequest("GET", "https://api.github.com/repos/"+*repo.Owner.Login+"/"+*repo.Name+"/rulesets/rule-suites", nil)
if err != nil {
return Invalid, err
}
resp, err = client.Do(context.Background(), req, nil)
switch resp.StatusCode {
case 403:
return AdministrationRead, nil
case 200:
return AdministrationWrite, nil
default:
return Invalid, err
}
}
func getCodeScanningAlertsPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
// Risk: Extremely Low
// -> GET request to /repos/{owner}/{repo}/code-scanning/alerts
_, resp, err := client.CodeScanning.ListAlertsForRepo(context.Background(), *repo.Owner.Login, *repo.Name, nil)
defer resp.Body.Close()
switch {
case resp.StatusCode == 403:
return NoAccess, nil
case resp.StatusCode == 404:
break
case resp.StatusCode >= 200 && resp.StatusCode <= 299:
break
default:
return Invalid, err
}
// Risk: Very Low
// -> Even if user had an alert with the number (see RANDOM_INTEGER above), this should error 422 due to the nil value passed in.
// -> PATCH request to /repos/{owner}/{repo}/code-scanning/alerts/{alert_number}
_, resp, err = client.CodeScanning.UpdateAlert(context.Background(), *repo.Owner.Login, *repo.Name, RANDOM_INTEGER, nil)
switch resp.StatusCode {
case 403:
return CodeScanningAlertsRead, nil
case 422:
return CodeScanningAlertsWrite, nil
case 200:
log.Fatal("This should never happen. We are updating an alert with nil which should be an invalid request.")
return CodeScanningAlertsWrite, nil
default:
return Invalid, err
}
}
func getCodespacesPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
// Risk: Extremely Low
// GET request to /repos/{owner}/{repo}/codespaces
_, resp, err := client.Codespaces.ListInRepo(context.Background(), *repo.Owner.Login, *repo.Name, nil)
switch resp.StatusCode {
case 403, 404:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Extremely Low
// GET request to /repos/{owner}/{repo}/codespaces/permissions_check
req, err := client.NewRequest("GET", "https://api.github.com/repos/"+*repo.Owner.Login+"/"+*repo.Name+"/codespaces/permissions_check", nil)
if err != nil {
return Invalid, err
}
resp, err = client.Do(context.Background(), req, nil)
switch resp.StatusCode {
case 403:
return CodespacesRead, nil
case 422:
return CodespacesWrite, nil
case 200:
return CodespacesWrite, nil
default:
return Invalid, err
}
}
func getCodespacesMetadataPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
// Risk: Extremely Low
// GET request to /repos/{owner}/{repo}/codespaces/machines
req, err := client.NewRequest("GET", "https://api.github.com/repos/"+*repo.Owner.Login+"/"+*repo.Name+"/codespaces/machines", nil)
if err != nil {
return Invalid, err
}
resp, err := client.Do(context.Background(), req, nil)
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200:
return CodespacesMetadataRead, nil
default:
return Invalid, err
}
}
func getCodespacesSecretsPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
// Risk: Extremely Low
// GET request to /repos/{owner}/{repo}/codespaces/secrets for non-existent secret
_, resp, err := client.Codespaces.GetRepoSecret(context.Background(), *repo.Owner.Login, *repo.Name, RANDOM_STRING)
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 404:
return CodespacesSecretsWrite, nil
case 200:
return CodespacesSecretsWrite, nil
default:
return Invalid, err
}
}
// getCommitStatusesPermission will check if we have access to commit statuses for a given repo.
// By default, we have read-only access to commit statuses for all public repos. If only public repos exist under
// this key's permissions, then they best we can hope for us a READ_WRITE status or an UNKNOWN status.
// If a private repo exists, then we can check for READ_ONLY, READ_WRITE and NO_ACCESS.
func getCommitStatusesPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
if *repo.Private {
// Risk: Extremely Low
// GET request to /repos/{owner}/{repo}/commits/{commit_sha}/statuses
_, resp, err := client.Repositories.ListStatuses(context.Background(), *repo.Owner.Login, *repo.Name, RANDOM_STRING, nil)
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 404:
break
default:
return Invalid, err
}
// At this point we have read access
// Risk: Extremely Low
// -> We're POSTing a commit status to a commit that cannot exist. This should always return 422 if valid access.
// POST request to /repos/{owner}/{repo}/statuses/{commit_sha}
_, resp, err = client.Repositories.CreateStatus(context.Background(), *repo.Owner.Login, *repo.Name, RANDOM_STRING, &gh.RepoStatus{})
switch resp.StatusCode {
case 403:
return CommitStatusesRead, nil
case 422:
return CommitStatusesWrite, nil
default:
return Invalid, err
}
} else {
// Will only land here if already tested one public repo and got a 403.
if currentAccess == NoAccess {
return NoAccess, nil
}
// Risk: Extremely Low
// -> We're POSTing a commit status to a commit that cannot exist. This should always return 422 if valid access.
// POST request to /repos/{owner}/{repo}/statuses/{commit_sha}
_, resp, err := client.Repositories.CreateStatus(context.Background(), *repo.Owner.Login, *repo.Name, RANDOM_STRING, &gh.RepoStatus{})
switch resp.StatusCode {
case 403:
// All we know is we don't have READ_WRITE
return NoAccess, nil
case 422:
return CommitStatusesWrite, nil
default:
return Invalid, err
}
}
}
// getContentsPermission will check if we have access to the contents of a given repo.
// By default, we have read-only access to the contents of all public repos. If only public repos exist under
// this key's permissions, then they best we can hope for us a READ_WRITE status or an UNKNOWN status.
// If a private repo exists, then we can check for READ_ONLY, READ_WRITE and NO_ACCESS.
func getContentsPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
if *repo.Private {
// Risk: Extremely Low
// GET request to /repos/{owner}/{repo}/commits
_, resp, err := client.Repositories.ListCommits(context.Background(), *repo.Owner.Login, *repo.Name, &gh.CommitsListOptions{})
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200:
break
case 409:
break
default:
return Invalid, err
}
// At this point we have read access
// Risk: Low-Medium
// -> We're creating a file with an invalid payload. Worst case is a file with a random string and no content is created. But this should never happen.
// PUT /repos/{owner}/{repo}/contents/{path}
_, resp, err = client.Repositories.CreateFile(context.Background(), *repo.Owner.Login, *repo.Name, RANDOM_STRING, &gh.RepositoryContentFileOptions{})
switch resp.StatusCode {
case 403:
return ContentsRead, nil
case 200:
log.Fatal("This should never happen. We are creating a file with an invalid payload.")
return ContentsWrite, nil
case 400, 422:
return ContentsWrite, nil
default:
return Invalid, err
}
} else {
// Will only land here if already tested one public repo and got a 403.
if currentAccess == NoAccess {
return NoAccess, nil
}
// Risk: Low-Medium
// -> We're creating a file with an invalid payload. Worst case is a file with a random string and no content is created. But this should never happen.
// PUT /repos/{owner}/{repo}/contents/{path}
_, resp, err := client.Repositories.CreateFile(context.Background(), *repo.Owner.Login, *repo.Name, RANDOM_STRING, &gh.RepositoryContentFileOptions{})
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200:
panic("This should never happen. We are creating a file with an invalid payload.")
case 400, 422:
return ContentsWrite, nil
default:
return Invalid, err
}
}
}
func getDependabotAlertsPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
// Risk: Extremely Low
// GET /repos/{owner}/{repo}/dependabot/alerts
_, resp, err := client.Dependabot.ListRepoAlerts(context.Background(), *repo.Owner.Login, *repo.Name, &gh.ListAlertsOptions{})
defer resp.Body.Close()
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// PATCH /repos/{owner}/{repo}/dependabot/alerts/{alert_number}
_, resp, err = client.Dependabot.UpdateAlert(context.Background(), *repo.Owner.Login, *repo.Name, RANDOM_INTEGER, nil)
switch resp.StatusCode {
case 403:
return DependabotAlertsRead, nil
case 422, 404:
return DependabotAlertsWrite, nil
case 200:
log.Fatal("This should never happen. We are updating an alert with nil which should be an invalid request.")
return DependabotAlertsWrite, nil
default:
return Invalid, err
}
}
func getDependabotSecretsPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
// Risk: Extremely Low
// GET /repos/{owner}/{repo}/dependabot/secrets
_, resp, err := client.Dependabot.ListRepoSecrets(context.Background(), *repo.Owner.Login, *repo.Name, &gh.ListOptions{})
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Very Low
// -> We're "creating" a secret with an invalid payload. Even if we did, the name would be (see RANDOM_STRING above) and the value would be nil.
// PUT /repos/{owner}/{repo}/dependabot/secrets/{secret_name}
resp, err = client.Dependabot.CreateOrUpdateRepoSecret(context.Background(), *repo.Owner.Login, *repo.Name, &gh.DependabotEncryptedSecret{Name: RANDOM_STRING})
switch resp.StatusCode {
case 403:
return DependabotSecretsRead, nil
case 422:
return DependabotSecretsWrite, nil
case 201, 204:
log.Fatal("This should never happen. We are creating a secret with an invalid payload.")
return DependabotSecretsWrite, nil
default:
return Invalid, err
}
}
func getDeploymentsPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
// Risk: Extremely Low
// GET /repos/{owner}/{repo}/deployments
_, resp, err := client.Repositories.ListDeployments(context.Background(), *repo.Owner.Login, *repo.Name, &gh.DeploymentsListOptions{})
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Very Low
// -> We're creating a deployment with an invalid payload. Even if we did, the name would be (see RANDOM_STRING above) and the value would be nil.
// POST /repos/{owner}/{repo}/deployments/{deployment_id}/statuses
_, resp, err = client.Repositories.CreateDeployment(context.Background(), *repo.Owner.Login, *repo.Name, &gh.DeploymentRequest{})
switch resp.StatusCode {
case 403:
return DeploymentsRead, nil
case 409, 422:
return DeploymentsWrite, nil
case 201, 202:
log.Fatal("This should never happen. We are creating a deployment with an invalid payload.")
return DeploymentsWrite, nil
default:
return Invalid, err
}
}
func getEnvironmentsPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
// Risk: Extremely Low
// GET /repos/{owner}/{repo}/environments
envResp, resp, _ := client.Repositories.ListEnvironments(context.Background(), *repo.Owner.Login, *repo.Name, &gh.EnvironmentListOptions{})
if resp.StatusCode != 200 {
return NoAccess, nil
}
// If no environments exist, then we return UNKNOWN
if len(envResp.Environments) == 0 {
return NoAccess, nil
}
// Risk: Extremely Low
// GET /repositories/{repository_id}/environments/{environment_name}/variables
_, resp, _ = client.Actions.ListEnvVariables(context.Background(), *repo.Owner.Login, *repo.Name, *envResp.Environments[0].Name, &gh.ListOptions{})
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200:
break
default:
return Invalid, nil
}
// Risk: Very Low
// -> We're updating an environment variable with an invalid payload. Even if we did, the name would be (see RANDOM_STRING above) and the value would be nil.
// PATCH /repositories/{repository_id}/environments/{environment_name}/variables/{variable_name}
resp, err := client.Actions.UpdateEnvVariable(context.Background(), *repo.Owner.Login, *repo.Name, *envResp.Environments[0].Name, &gh.ActionsVariable{Name: RANDOM_STRING})
switch resp.StatusCode {
case 403:
return EnvironmentsRead, nil
case 422:
return EnvironmentsWrite, nil
case 200:
log.Fatal("This should never happen. We are updating an environment variable with an invalid payload.")
return EnvironmentsWrite, nil
default:
return Invalid, err
}
}
func getIssuesPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
if *repo.Private {
// Risk: Extremely Low
// GET /repos/{owner}/{repo}/issues
_, resp, err := client.Issues.ListByRepo(context.Background(), *repo.Owner.Login, *repo.Name, &gh.IssueListByRepoOptions{})
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200, 301:
break
default:
return Invalid, err
}
// Risk: Very Low
// -> We're editing an issue label that does not exist. Even if we did, the name would be (see RANDOM_STRING above).
// PATCH /repos/{owner}/{repo}/labels/{name}
_, resp, err = client.Issues.EditLabel(context.Background(), *repo.Owner.Login, *repo.Name, RANDOM_STRING, &gh.Label{})
switch resp.StatusCode {
case 403:
return IssuesRead, nil
case 404:
return IssuesWrite, nil
case 200:
log.Fatal("This should never happen. We are editing a label with an invalid payload.")
return IssuesWrite, nil
default:
return Invalid, err
}
} else {
// Will only land here if already tested one public repo and got a 403.
if currentAccess == NoAccess {
return NoAccess, nil
}
// Risk: Very Low
// -> We're editing an issue label that does not exist. Even if we did, the name would be (see RANDOM_STRING above).
// PATCH /repos/{owner}/{repo}/labels/{name}
_, resp, err := client.Issues.EditLabel(context.Background(), *repo.Owner.Login, *repo.Name, RANDOM_STRING, &gh.Label{})
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 404:
return IssuesWrite, nil
case 200:
log.Fatal("This should never happen. We are editing a label with an invalid payload.")
return IssuesWrite, nil
default:
return Invalid, err
}
}
}
func getPagesPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
if *repo.Private {
// Risk: Extremely Low
// GET /repos/{owner}/{repo}/pages
_, resp, err := client.Repositories.GetPagesInfo(context.Background(), *repo.Owner.Login, *repo.Name)
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200, 404:
break
default:
return Invalid, err
}
// Risk: Very Low
// -> We're cancelling a GitHub Pages deployment that does not exist (see RANDOM_STRING above).
// POST /repos/{owner}/{repo}/pages/deployments/{deployment_id}/cancel
req, err := client.NewRequest("POST", "https://api.github.com/repos/"+*repo.Owner.Login+"/"+*repo.Name+"/pages/deployments/"+RANDOM_STRING+"/cancel", nil)
if err != nil {
return Invalid, err
}
resp, err = client.Do(context.Background(), req, nil)
switch resp.StatusCode {
case 403:
return PagesRead, nil
case 404:
return PagesWrite, nil
case 200:
log.Fatal("This should never happen. We are cancelling a deployment with an invalid ID.")
return PagesWrite, nil
default:
return Invalid, err
}
} else {
// Will only land here if already tested one public repo and got a 403.
if currentAccess == NoAccess {
return NoAccess, nil
}
// Risk: Very Low
// -> We're cancelling a GitHub Pages deployment that does not exist (see RANDOM_STRING above).
// POST /repos/{owner}/{repo}/pages/deployments/{deployment_id}/cancel
req, err := client.NewRequest("POST", "https://api.github.com/repos/"+*repo.Owner.Login+"/"+*repo.Name+"/pages/deployments/"+RANDOM_STRING+"/cancel", nil)
if err != nil {
return Invalid, err
}
resp, err := client.Do(context.Background(), req, nil)
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 404:
return PagesWrite, nil
case 200:
log.Fatal("This should never happen. We are cancelling a deployment with an invalid ID.")
return PagesWrite, nil
default:
return Invalid, err
}
}
}
func getPullRequestsPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
if *repo.Private {
// Risk: Extremely Low
// GET /repos/{owner}/{repo}/pulls
_, resp, err := client.PullRequests.List(context.Background(), *repo.Owner.Login, *repo.Name, &gh.PullRequestListOptions{})
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Very Low
// -> We're creating a pull request with an invalid payload.
// POST /repos/{owner}/{repo}/pulls
_, resp, err = client.PullRequests.Create(context.Background(), *repo.Owner.Login, *repo.Name, &gh.NewPullRequest{})
switch resp.StatusCode {
case 403:
return PullRequestsRead, nil
case 422:
return PullRequestsWrite, nil
case 200:
log.Fatal("This should never happen. We are creating a pull request with an invalid payload.")
return PullRequestsWrite, nil
default:
return Invalid, err
}
} else {
// Will only land here if already tested one public repo and got a 403.
if currentAccess == NoAccess {
return NoAccess, nil
}
// Risk: Very Low
// -> We're creating a pull request with an invalid payload.
// POST /repos/{owner}/{repo}/pulls
_, resp, err := client.PullRequests.Create(context.Background(), *repo.Owner.Login, *repo.Name, &gh.NewPullRequest{})
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 422:
return PullRequestsWrite, nil
case 200:
log.Fatal("This should never happen. We are creating a pull request with an invalid payload.")
return PullRequestsWrite, nil
default:
return Invalid, err
}
}
}
func getRepoSecurityPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
if *repo.Private {
// Risk: Extremely Low
// GET /repos/{owner}/{repo}/security-advisories
_, resp, err := client.SecurityAdvisories.ListRepositorySecurityAdvisories(context.Background(), *repo.Owner.Login, *repo.Name, nil)
switch resp.StatusCode {
case 403, 404:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Very Low
// -> We're creating a security advisory with an invalid payload.
// POST /repos/{owner}/{repo}/security-advisories
req, err := client.NewRequest("POST", "https://api.github.com/repos/"+*repo.Owner.Login+"/"+*repo.Name+"/security-advisories", nil)
if err != nil {
return Invalid, err
}
resp, err = client.Do(context.Background(), req, nil)
switch resp.StatusCode {
case 403:
return RepoSecurityRead, nil
case 422:
return RepoSecurityWrite, nil
case 200:
log.Fatal("This should never happen. We are creating a security advisory with an invalid payload.")
return RepoSecurityWrite, nil
default:
return Invalid, err
}
} else {
// Will only land here if already tested one public repo and got a 403.
if currentAccess == NoAccess {
return NoAccess, nil
}
// Risk: Very Low
// -> We're creating a security advisory with an invalid payload.
// POST /repos/{owner}/{repo}/security-advisories
req, err := client.NewRequest("POST", "https://api.github.com/repos/"+*repo.Owner.Login+"/"+*repo.Name+"/security-advisories", nil)
if err != nil {
return Invalid, err
}
resp, err := client.Do(context.Background(), req, nil)
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 422:
return RepoSecurityWrite, nil
case 200:
log.Fatal("This should never happen. We are creating a security advisory with an invalid payload.")
return RepoSecurityWrite, nil
default:
return Invalid, err
}
}
}
func getSecretScanningPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
// Risk: Extremely Low
// GET /repos/{owner}/{repo}/secret-scanning/alerts
_, resp, err := client.SecretScanning.ListAlertsForRepo(context.Background(), *repo.Owner.Login, *repo.Name, nil)
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200, 404:
break
default:
return Invalid, err
}
// Risk: Very Low
// -> We're updating a secret scanning alert for an alert that doesn't exist.
// POST /repos/{owner}/{repo}/secret-scanning/alerts
_, resp, err = client.SecretScanning.UpdateAlert(context.Background(), *repo.Owner.Login, *repo.Name, RANDOM_INTEGER, &gh.SecretScanningAlertUpdateOptions{})
switch resp.StatusCode {
case 403:
return SecretScanningRead, nil
case 404, 422:
return SecretScanningWrite, nil
case 200:
log.Fatal("This should never happen. We are updating a secret scanning alert that doesn't exist.")
return SecretScanningWrite, nil
default:
return Invalid, err
}
}
func getSecretsPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
// Risk: Extremely Low
// GET /repos/{owner}/{repo}/actions/secrets
_, resp, err := client.Actions.ListRepoSecrets(context.Background(), *repo.Owner.Login, *repo.Name, &gh.ListOptions{})
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Very Low
// -> We're creating a secret with an invalid payload.
// PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}
resp, err = client.Actions.CreateOrUpdateRepoSecret(context.Background(), *repo.Owner.Login, *repo.Name, &gh.EncryptedSecret{Name: RANDOM_STRING})
switch resp.StatusCode {
case 403:
return SecretsRead, nil
case 422:
return SecretsWrite, nil
case 201, 204:
log.Fatal("This should never happen. We are creating a secret with an invalid payload.")
return SecretsWrite, nil
default:
return Invalid, err
}
}
func getVariablesPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
// Risk: Extremely Low
// GET /repos/{owner}/{repo}/actions/variables
_, resp, err := client.Actions.ListRepoVariables(context.Background(), *repo.Owner.Login, *repo.Name, &gh.ListOptions{})
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Very Low
// -> We're updating a variable that doesn't exist with an invalid payload.
// PATCH /repos/{owner}/{repo}/actions/variables/{name}
resp, err = client.Actions.UpdateRepoVariable(context.Background(), *repo.Owner.Login, *repo.Name, &gh.ActionsVariable{Name: RANDOM_STRING})
switch resp.StatusCode {
case 403:
return VariablesRead, nil
case 422:
return VariablesWrite, nil
case 201, 204:
log.Fatal("This should never happen. We are patching a variable with an invalid payload and no name.")
return VariablesWrite, nil
default:
return Invalid, err
}
}
func getWebhooksPermission(client *gh.Client, repo *gh.Repository, currentAccess Permission) (Permission, error) {
// Risk: Extremely Low
// GET /repos/{owner}/{repo}/hooks
_, resp, err := client.Repositories.ListHooks(context.Background(), *repo.Owner.Login, *repo.Name, &gh.ListOptions{})
switch resp.StatusCode {
case 403, 404:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Very Low
// -> We're updating a webhook that doesn't exist with an invalid payload.
// PATCH /repos/{owner}/{repo}/hooks/{hook_id}
_, resp, err = client.Repositories.EditHook(context.Background(), *repo.Owner.Login, *repo.Name, RANDOM_INTEGER, &gh.Hook{})
switch resp.StatusCode {
case 403:
return WebhooksRead, nil
case 404:
return WebhooksWrite, nil
case 200:
log.Fatal("This should never happen. We are updating a webhook with an invalid payload.")
return WebhooksWrite, nil
default:
return Invalid, err
}
}
// analyzeRepositoryPermissions will analyze the fine-grained permissions of a given permission type and return the access level.
// This function is needed b/c in some cases a token could have permissions that are only enabled on specific repos.
// If we only checked one repo, we wouldn't be able to tell if the token has access to a specific permission type.
// Ex: "Code scanning alerts" must be enabled to tell if we have that permission.
func analyzeRepositoryPermissions(client *gh.Client, repos []*gh.Repository) ([]Permission, error) {
perms := make([]Permission, len(repoPermFuncMap))
for _, repo := range repos {
for i, permFunc := range repoPermFuncMap {
access, err := permFunc(client, repo, perms[i])
if err != nil || access == Invalid {
// TODO: Log error.
continue
}
if perms[i] == Invalid || perms[i] == NoAccess {
perms[i] = access
}
}
}
return perms, nil
}
func getBlockUserPermission(client *gh.Client, user *gh.User) (Permission, error) {
// Risk: Extremely Low
// -> GET request to /user/blocks
_, resp, err := client.Users.ListBlockedUsers(context.Background(), nil)
switch resp.StatusCode {
case 403, 404:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Extremely Low
// -> PUT request to /user/blocks/{username}
// -> We're blocking a user that doesn't exist. See RANDOM_STRING above.
resp, err = client.Users.BlockUser(context.Background(), RANDOM_STRING)
switch resp.StatusCode {
case 403:
return BlockUserRead, nil
case 404:
return BlockUserWrite, nil
case 204:
log.Fatal("This should never happen. We are blocking a user that doesn't exist.")
return BlockUserWrite, nil
default:
return Invalid, err
}
}
func getCodespacesUserPermission(client *gh.Client, user *gh.User) (Permission, error) {
// Risk: Extremely Low
// GET request to /user/codespaces/secrets
_, resp, err := client.Codespaces.ListUserSecrets(context.Background(), nil)
switch resp.StatusCode {
case 403, 404:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Low
// PUT request to /user/codespaces/secrets/{secret_name}
// Payload is invalid, so it shouldn't actually post.
resp, err = client.Codespaces.CreateOrUpdateUserSecret(context.Background(), &gh.EncryptedSecret{Name: RANDOM_STRING})
switch resp.StatusCode {
case 403:
return CodespaceUserSecretsRead, nil
case 422:
return CodespaceUserSecretsWrite, nil
case 201, 204:
log.Fatal("This should never happen. We are creating a user secret with an invalid payload.")
return CodespaceUserSecretsWrite, nil
default:
return Invalid, err
}
}
func getEmailPermission(client *gh.Client, user *gh.User) (Permission, error) {
// Risk: Extremely Low
// GET request to /user/emails
_, resp, err := client.Users.ListEmails(context.Background(), nil)
switch resp.StatusCode {
case 403, 404:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Low
// POST request to /user/emails/visibility
_, resp, err = client.Users.SetEmailVisibility(context.Background(), RANDOM_STRING)
switch resp.StatusCode {
case 403, 404:
return EmailRead, nil
case 422:
return EmailWrite, nil
case 201:
log.Fatal("This should never happen. We are setting email visibility with an invalid payload.")
return EmailWrite, nil
default:
return Invalid, err
}
}
func getFollowersPermission(client *gh.Client, user *gh.User) (Permission, error) {
// Risk: Extremely Low
// GET request to /user/followers
_, resp, err := client.Users.ListFollowers(context.Background(), "", nil)
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Low - Medium
// DELETE request to /user/followers/{username}
// For the username value, we need to use a real username. So there is a super small chance that someone following
// an account for RANDOM_USERNAME value will then no longer follow that account.
// But we're using an account created specifically for this purpose with no activity.
resp, err = client.Users.Unfollow(context.Background(), RANDOM_USERNAME)
switch resp.StatusCode {
case 403, 404:
return FollowersRead, nil
case 204:
return FollowersWrite, nil
default:
return Invalid, err
}
}
func getGPGKeysPermission(client *gh.Client, user *gh.User) (Permission, error) {
// Risk: Extremely Low
// GET request to /user/gpg_keys
_, resp, err := client.Users.ListGPGKeys(context.Background(), "", nil)
switch resp.StatusCode {
case 403, 404:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Low - Medium
// POST request to /user/gpg_keys
// Payload is invalid, so it shouldn't actually post.
_, resp, err = client.Users.CreateGPGKey(context.Background(), RANDOM_STRING)
switch resp.StatusCode {
case 403:
return GpgKeysRead, nil
case 422:
return GpgKeysWrite, nil
case 200, 201, 204:
log.Fatal("This should never happen. We are creating a GPG key with an invalid payload.")
return GpgKeysWrite, nil
default:
return Invalid, err
}
}
func getGistsPermission(client *gh.Client, user *gh.User) (Permission, error) {
// Risk: Low - Medium
// POST request to /gists
// Payload is invalid, so it shouldn't actually post.
_, resp, err := client.Gists.Create(context.Background(), &gh.Gist{})
switch resp.StatusCode {
case 403, 404:
return NoAccess, nil
case 422:
return GistsWrite, nil
case 200, 201, 204:
log.Fatal("This should never happen. We are creating a Gist with an invalid payload.")
return GistsWrite, nil
default:
return Invalid, err
}
}
func getGitKeysPermission(client *gh.Client, user *gh.User) (Permission, error) {
// Risk: Extremely Low
// GET request to /user/keys
_, resp, err := client.Users.ListKeys(context.Background(), "", nil)
switch resp.StatusCode {
case 403, 404:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Low - Medium
// POST request to /user/keys
// Payload is invalid, so it shouldn't actually post.
_, resp, err = client.Users.CreateKey(context.Background(), &gh.Key{})
switch resp.StatusCode {
case 403:
return GitKeysRead, nil
case 422:
return GitKeysWrite, nil
case 200, 201, 204:
log.Fatal("This should never happen. We are creating a key with an invalid payload.")
return GitKeysWrite, nil
default:
return Invalid, err
}
}
func getLimitsPermission(client *gh.Client, user *gh.User) (Permission, error) {
// Risk: Extremely Low
// GET request to /user/interaction-limits
req, err := client.NewRequest("GET", "https://api.github.com/user/interaction-limits", nil)
if err != nil {
return Invalid, err
}
resp, err := client.Do(context.Background(), req, nil)
switch resp.StatusCode {
case 403:
return NoAccess, nil
case 200, 204:
break
default:
return Invalid, err
}
// Risk: Low
// PUT request to /user/interaction-limits
// Payload is invalid, so it shouldn't actually post.
req, err = client.NewRequest("PUT", "https://api.github.com/user/interaction-limits", nil)
if err != nil {
return Invalid, err
}
resp, err = client.Do(context.Background(), req, nil)
switch resp.StatusCode {
case 403:
return LimitsRead, nil
case 422:
return LimitsWrite, nil
case 200, 204:
log.Fatal("This should never happen. We are setting interaction limits with an invalid payload.")
return LimitsWrite, nil
default:
return Invalid, err
}
}
func getPlanPermission(client *gh.Client, user *gh.User) (Permission, error) {
// Risk: Extremely Low
// GET request to /user/{username}/settings/billing/actions
_, resp, err := client.Billing.GetActionsBillingUser(context.Background(), *user.Login)
switch resp.StatusCode {
case 403, 404:
return NoAccess, nil
case 200:
return PlanRead, nil
default:
return Invalid, err
}
}
func getProfilePermission(client *gh.Client, user *gh.User) (Permission, error) {
// Risk: Low
// POST request to /user/social_accounts
// Payload is invalid, so it shouldn't actually patch.
req, err := client.NewRequest("POST", "https://api.github.com/user/social_accounts", nil)
if err != nil {
return Invalid, err
}
resp, err := client.Do(context.Background(), req, nil)
switch resp.StatusCode {
case 403, 404:
return NoAccess, nil
case 422:
return ProfileWrite, nil
case 200, 201, 204:
log.Fatal("This should never happen. We are creating a social account with an invalid payload.")
return ProfileWrite, nil
default:
return Invalid, err
}
}
func getSigningKeysPermission(client *gh.Client, user *gh.User) (Permission, error) {
// Risk: Extremely Low
// GET request to /user/ssh_signing_keys
_, resp, err := client.Users.ListSSHSigningKeys(context.Background(), "", nil)
switch resp.StatusCode {
case 403, 404:
return NoAccess, nil
case 200:
break
default:
return Invalid, err
}
// Risk: Low - Medium
// POST request to /user/ssh_signing_keys
// Payload is invalid, so it shouldn't actually post.
_, resp, err = client.Users.CreateSSHSigningKey(context.Background(), &gh.Key{})
switch resp.StatusCode {
case 403:
return SigningKeysRead, nil
case 422:
return SigningKeysWrite, nil
case 200, 201, 204:
log.Fatal("This should never happen. We are creating a SSH key with an invalid payload.")
return SigningKeysWrite, nil
default:
return Invalid, err
}
}
func getStarringPermission(client *gh.Client, user *gh.User) (Permission, error) {
// Note: We can't test READ_WRITE b/c Unstar() isn't working even with READ_WRITE permissions.
// Note: GET /user/starred returns the same results regardless of permissions
// but since all have the same access, we'll call it READ_ONLY for now.
return StarringRead, nil
}
func getWatchingPermission(client *gh.Client, user *gh.User) (Permission, error) {
// Note: GET /user/subscriptions returns the same results regardless of permissions
// but since all have the same access, we'll call it READ_ONLY for now.
return WatchingRead, nil
}
func analyzeUserPermissions(client *gh.Client, user *gh.User) ([]Permission, error) {
perms := []Permission{}
for _, permFunc := range acctPermFuncMap {
access, err := permFunc(client, user)
if err != nil {
// TODO: Log error.
continue
}
perms = append(perms, access)
}
return perms, nil
}
func AnalyzeFineGrainedToken(client *gh.Client, meta *common.TokenMetadata, shallowCheck bool) (*common.SecretInfo, error) {
allRepos, err := common.GetAllReposForUser(client)
if err != nil {
return nil, err
}
allGists, err := common.GetAllGistsForUser(client)
if err != nil {
return nil, err
}
accessibleRepos := make([]*gh.Repository, 0)
for _, repo := range allRepos {
perm, err := getMetadataPermission(client, repo, Invalid)
if err != nil {
// TODO: Log error.
continue
}
if perm != Invalid {
accessibleRepos = append(accessibleRepos, repo)
}
}
repoAccessMap := []Permission{}
userAccessMap := []Permission{}
if !shallowCheck {
// Check our access
perms, err := analyzeRepositoryPermissions(client, accessibleRepos)
if err != nil {
return nil, err
}
for _, perm := range perms {
if perm != Invalid && perm != NoAccess {
repoAccessMap = append(repoAccessMap, perm)
}
}
perms, err = analyzeUserPermissions(client, meta.User)
if err != nil {
return nil, err
}
for _, perm := range perms {
if perm != Invalid && perm != NoAccess {
userAccessMap = append(userAccessMap, perm)
}
}
}
return &common.SecretInfo{
Metadata: meta,
Repos: allRepos,
Gists: allGists,
AccessibleRepos: accessibleRepos,
RepoAccessMap: repoAccessMap,
UserAccessMap: userAccessMap,
}, nil
}
func PrintFineGrainedToken(cfg *config.Config, info *common.SecretInfo) {
if len(info.AccessibleRepos) == 0 {
// If no repos are accessible, then we only have read access to public repos
color.Red("[!] Repository Access: Public Repositories (read-only)\n")
} else {
// Print out the repos the token can access
color.Green(fmt.Sprintf("Found %v", len(info.AccessibleRepos)) + " Accessible Repositor(ies) \n")
common.PrintGitHubRepos(info.AccessibleRepos)
// Print out the access map
perms, ok := info.RepoAccessMap.([]Permission)
if !ok {
panic("Repo Access Map is not of type Permission")
}
printFineGrainedPermissions(perms, cfg.ShowAll, true)
}
perms, ok := info.UserAccessMap.([]Permission)
if !ok {
panic("Repo Access Map is not of type Permission")
}
printFineGrainedPermissions(perms, cfg.ShowAll, false)
common.PrintGists(info.Gists, cfg.ShowAll)
}
func printFineGrainedPermissions(accessMap []Permission, showAll bool, repoPermissions bool) {
permissionCount := 0
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Permission Type", "Permission" /* Add more column headers if needed */})
for _, perm := range accessMap {
permStr, _ := perm.ToString()
if perm == Invalid {
// don't change permissionCount
} else {
permissionCount++
}
if !showAll && perm == Invalid {
continue
} else {
k, v := permissionFormatter(permStr, perm)
t.AppendRow([]any{k, v})
}
}
var permissionType string
if repoPermissions {
permissionType = "Repositor(ies)"
} else {
permissionType = "User Account"
}
if permissionCount == 0 && !showAll {
color.Red("No Permissions Found for the %v above\n\n", permissionType)
return
} else if permissionCount == 0 {
color.Red("Found No Permissions for the %v above\n", permissionType)
} else {
color.Green(fmt.Sprintf("Found %v Permission(s) for the %v above\n", permissionCount, permissionType))
}
t.Render()
fmt.Print("\n\n")
}
================================================
FILE: pkg/analyzer/analyzers/github/finegrained/finegrained.yaml
================================================
# Please generate a yaml list of all of the strings permission_name:access_level for all of the permissions and access levels that can be emitted from the test functions. The strings should be lower snake case with a colon joining the permission name and access level. The only access levels I want are "read" and "write"
permissions:
- no_access
- actions:read
- actions:write
- administration:read
- administration:write
- code_scanning_alerts:read
- code_scanning_alerts:write
- codespaces:read
- codespaces:write
- codespaces_lifecycle:read
- codespaces_lifecycle:write
- codespaces_metadata:read
- codespaces_metadata:write
- codespaces_secrets:read
- codespaces_secrets:write
- commit_statuses:read
- commit_statuses:write
- contents:read
- contents:write
- custom_properties:read
- custom_properties:write
- dependabot_alerts:read
- dependabot_alerts:write
- dependabot_secrets:read
- dependabot_secrets:write
- deployments:read
- deployments:write
- environments:read
- environments:write
- issues:read
- issues:write
- merge_queues:read
- merge_queues:write
- metadata:read
- metadata:write
- pages:read
- pages:write
- pull_requests:read
- pull_requests:write
- repo_security:read
- repo_security:write
- secret_scanning:read
- secret_scanning:write
- secrets:read
- secrets:write
- variables:read
- variables:write
- webhooks:read
- webhooks:write
- workflows:read
- workflows:write
- block_user:read
- block_user:write
- codespace_user_secrets:read
- codespace_user_secrets:write
- email:read
- email:write
- followers:read
- followers:write
- gpg_keys:read
- gpg_keys:write
- gists:read
- gists:write
- git_keys:read
- git_keys:write
- limits:read
- limits:write
- plan:read
- plan:write
- private_invites:read
- private_invites:write
- profile:read
- profile:write
- signing_keys:read
- signing_keys:write
- starring:read
- starring:write
- watching:read
- watching:write
================================================
FILE: pkg/analyzer/analyzers/github/finegrained/finegrained_permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package finegrained
import "errors"
type Permission int
const (
Invalid Permission = iota
NoAccess Permission = iota
ActionsRead Permission = iota
ActionsWrite Permission = iota
AdministrationRead Permission = iota
AdministrationWrite Permission = iota
CodeScanningAlertsRead Permission = iota
CodeScanningAlertsWrite Permission = iota
CodespacesRead Permission = iota
CodespacesWrite Permission = iota
CodespacesLifecycleRead Permission = iota
CodespacesLifecycleWrite Permission = iota
CodespacesMetadataRead Permission = iota
CodespacesMetadataWrite Permission = iota
CodespacesSecretsRead Permission = iota
CodespacesSecretsWrite Permission = iota
CommitStatusesRead Permission = iota
CommitStatusesWrite Permission = iota
ContentsRead Permission = iota
ContentsWrite Permission = iota
CustomPropertiesRead Permission = iota
CustomPropertiesWrite Permission = iota
DependabotAlertsRead Permission = iota
DependabotAlertsWrite Permission = iota
DependabotSecretsRead Permission = iota
DependabotSecretsWrite Permission = iota
DeploymentsRead Permission = iota
DeploymentsWrite Permission = iota
EnvironmentsRead Permission = iota
EnvironmentsWrite Permission = iota
IssuesRead Permission = iota
IssuesWrite Permission = iota
MergeQueuesRead Permission = iota
MergeQueuesWrite Permission = iota
MetadataRead Permission = iota
MetadataWrite Permission = iota
PagesRead Permission = iota
PagesWrite Permission = iota
PullRequestsRead Permission = iota
PullRequestsWrite Permission = iota
RepoSecurityRead Permission = iota
RepoSecurityWrite Permission = iota
SecretScanningRead Permission = iota
SecretScanningWrite Permission = iota
SecretsRead Permission = iota
SecretsWrite Permission = iota
VariablesRead Permission = iota
VariablesWrite Permission = iota
WebhooksRead Permission = iota
WebhooksWrite Permission = iota
WorkflowsRead Permission = iota
WorkflowsWrite Permission = iota
BlockUserRead Permission = iota
BlockUserWrite Permission = iota
CodespaceUserSecretsRead Permission = iota
CodespaceUserSecretsWrite Permission = iota
EmailRead Permission = iota
EmailWrite Permission = iota
FollowersRead Permission = iota
FollowersWrite Permission = iota
GpgKeysRead Permission = iota
GpgKeysWrite Permission = iota
GistsRead Permission = iota
GistsWrite Permission = iota
GitKeysRead Permission = iota
GitKeysWrite Permission = iota
LimitsRead Permission = iota
LimitsWrite Permission = iota
PlanRead Permission = iota
PlanWrite Permission = iota
PrivateInvitesRead Permission = iota
PrivateInvitesWrite Permission = iota
ProfileRead Permission = iota
ProfileWrite Permission = iota
SigningKeysRead Permission = iota
SigningKeysWrite Permission = iota
StarringRead Permission = iota
StarringWrite Permission = iota
WatchingRead Permission = iota
WatchingWrite Permission = iota
)
var (
PermissionStrings = map[Permission]string{
NoAccess: "no_access",
ActionsRead: "actions:read",
ActionsWrite: "actions:write",
AdministrationRead: "administration:read",
AdministrationWrite: "administration:write",
CodeScanningAlertsRead: "code_scanning_alerts:read",
CodeScanningAlertsWrite: "code_scanning_alerts:write",
CodespacesRead: "codespaces:read",
CodespacesWrite: "codespaces:write",
CodespacesLifecycleRead: "codespaces_lifecycle:read",
CodespacesLifecycleWrite: "codespaces_lifecycle:write",
CodespacesMetadataRead: "codespaces_metadata:read",
CodespacesMetadataWrite: "codespaces_metadata:write",
CodespacesSecretsRead: "codespaces_secrets:read",
CodespacesSecretsWrite: "codespaces_secrets:write",
CommitStatusesRead: "commit_statuses:read",
CommitStatusesWrite: "commit_statuses:write",
ContentsRead: "contents:read",
ContentsWrite: "contents:write",
CustomPropertiesRead: "custom_properties:read",
CustomPropertiesWrite: "custom_properties:write",
DependabotAlertsRead: "dependabot_alerts:read",
DependabotAlertsWrite: "dependabot_alerts:write",
DependabotSecretsRead: "dependabot_secrets:read",
DependabotSecretsWrite: "dependabot_secrets:write",
DeploymentsRead: "deployments:read",
DeploymentsWrite: "deployments:write",
EnvironmentsRead: "environments:read",
EnvironmentsWrite: "environments:write",
IssuesRead: "issues:read",
IssuesWrite: "issues:write",
MergeQueuesRead: "merge_queues:read",
MergeQueuesWrite: "merge_queues:write",
MetadataRead: "metadata:read",
MetadataWrite: "metadata:write",
PagesRead: "pages:read",
PagesWrite: "pages:write",
PullRequestsRead: "pull_requests:read",
PullRequestsWrite: "pull_requests:write",
RepoSecurityRead: "repo_security:read",
RepoSecurityWrite: "repo_security:write",
SecretScanningRead: "secret_scanning:read",
SecretScanningWrite: "secret_scanning:write",
SecretsRead: "secrets:read",
SecretsWrite: "secrets:write",
VariablesRead: "variables:read",
VariablesWrite: "variables:write",
WebhooksRead: "webhooks:read",
WebhooksWrite: "webhooks:write",
WorkflowsRead: "workflows:read",
WorkflowsWrite: "workflows:write",
BlockUserRead: "block_user:read",
BlockUserWrite: "block_user:write",
CodespaceUserSecretsRead: "codespace_user_secrets:read",
CodespaceUserSecretsWrite: "codespace_user_secrets:write",
EmailRead: "email:read",
EmailWrite: "email:write",
FollowersRead: "followers:read",
FollowersWrite: "followers:write",
GpgKeysRead: "gpg_keys:read",
GpgKeysWrite: "gpg_keys:write",
GistsRead: "gists:read",
GistsWrite: "gists:write",
GitKeysRead: "git_keys:read",
GitKeysWrite: "git_keys:write",
LimitsRead: "limits:read",
LimitsWrite: "limits:write",
PlanRead: "plan:read",
PlanWrite: "plan:write",
PrivateInvitesRead: "private_invites:read",
PrivateInvitesWrite: "private_invites:write",
ProfileRead: "profile:read",
ProfileWrite: "profile:write",
SigningKeysRead: "signing_keys:read",
SigningKeysWrite: "signing_keys:write",
StarringRead: "starring:read",
StarringWrite: "starring:write",
WatchingRead: "watching:read",
WatchingWrite: "watching:write",
}
StringToPermission = map[string]Permission{
"no_access": NoAccess,
"actions:read": ActionsRead,
"actions:write": ActionsWrite,
"administration:read": AdministrationRead,
"administration:write": AdministrationWrite,
"code_scanning_alerts:read": CodeScanningAlertsRead,
"code_scanning_alerts:write": CodeScanningAlertsWrite,
"codespaces:read": CodespacesRead,
"codespaces:write": CodespacesWrite,
"codespaces_lifecycle:read": CodespacesLifecycleRead,
"codespaces_lifecycle:write": CodespacesLifecycleWrite,
"codespaces_metadata:read": CodespacesMetadataRead,
"codespaces_metadata:write": CodespacesMetadataWrite,
"codespaces_secrets:read": CodespacesSecretsRead,
"codespaces_secrets:write": CodespacesSecretsWrite,
"commit_statuses:read": CommitStatusesRead,
"commit_statuses:write": CommitStatusesWrite,
"contents:read": ContentsRead,
"contents:write": ContentsWrite,
"custom_properties:read": CustomPropertiesRead,
"custom_properties:write": CustomPropertiesWrite,
"dependabot_alerts:read": DependabotAlertsRead,
"dependabot_alerts:write": DependabotAlertsWrite,
"dependabot_secrets:read": DependabotSecretsRead,
"dependabot_secrets:write": DependabotSecretsWrite,
"deployments:read": DeploymentsRead,
"deployments:write": DeploymentsWrite,
"environments:read": EnvironmentsRead,
"environments:write": EnvironmentsWrite,
"issues:read": IssuesRead,
"issues:write": IssuesWrite,
"merge_queues:read": MergeQueuesRead,
"merge_queues:write": MergeQueuesWrite,
"metadata:read": MetadataRead,
"metadata:write": MetadataWrite,
"pages:read": PagesRead,
"pages:write": PagesWrite,
"pull_requests:read": PullRequestsRead,
"pull_requests:write": PullRequestsWrite,
"repo_security:read": RepoSecurityRead,
"repo_security:write": RepoSecurityWrite,
"secret_scanning:read": SecretScanningRead,
"secret_scanning:write": SecretScanningWrite,
"secrets:read": SecretsRead,
"secrets:write": SecretsWrite,
"variables:read": VariablesRead,
"variables:write": VariablesWrite,
"webhooks:read": WebhooksRead,
"webhooks:write": WebhooksWrite,
"workflows:read": WorkflowsRead,
"workflows:write": WorkflowsWrite,
"block_user:read": BlockUserRead,
"block_user:write": BlockUserWrite,
"codespace_user_secrets:read": CodespaceUserSecretsRead,
"codespace_user_secrets:write": CodespaceUserSecretsWrite,
"email:read": EmailRead,
"email:write": EmailWrite,
"followers:read": FollowersRead,
"followers:write": FollowersWrite,
"gpg_keys:read": GpgKeysRead,
"gpg_keys:write": GpgKeysWrite,
"gists:read": GistsRead,
"gists:write": GistsWrite,
"git_keys:read": GitKeysRead,
"git_keys:write": GitKeysWrite,
"limits:read": LimitsRead,
"limits:write": LimitsWrite,
"plan:read": PlanRead,
"plan:write": PlanWrite,
"private_invites:read": PrivateInvitesRead,
"private_invites:write": PrivateInvitesWrite,
"profile:read": ProfileRead,
"profile:write": ProfileWrite,
"signing_keys:read": SigningKeysRead,
"signing_keys:write": SigningKeysWrite,
"starring:read": StarringRead,
"starring:write": StarringWrite,
"watching:read": WatchingRead,
"watching:write": WatchingWrite,
}
PermissionIDs = map[Permission]int{
NoAccess: 1,
ActionsRead: 2,
ActionsWrite: 3,
AdministrationRead: 4,
AdministrationWrite: 5,
CodeScanningAlertsRead: 6,
CodeScanningAlertsWrite: 7,
CodespacesRead: 8,
CodespacesWrite: 9,
CodespacesLifecycleRead: 10,
CodespacesLifecycleWrite: 11,
CodespacesMetadataRead: 12,
CodespacesMetadataWrite: 13,
CodespacesSecretsRead: 14,
CodespacesSecretsWrite: 15,
CommitStatusesRead: 16,
CommitStatusesWrite: 17,
ContentsRead: 18,
ContentsWrite: 19,
CustomPropertiesRead: 20,
CustomPropertiesWrite: 21,
DependabotAlertsRead: 22,
DependabotAlertsWrite: 23,
DependabotSecretsRead: 24,
DependabotSecretsWrite: 25,
DeploymentsRead: 26,
DeploymentsWrite: 27,
EnvironmentsRead: 28,
EnvironmentsWrite: 29,
IssuesRead: 30,
IssuesWrite: 31,
MergeQueuesRead: 32,
MergeQueuesWrite: 33,
MetadataRead: 34,
MetadataWrite: 35,
PagesRead: 36,
PagesWrite: 37,
PullRequestsRead: 38,
PullRequestsWrite: 39,
RepoSecurityRead: 40,
RepoSecurityWrite: 41,
SecretScanningRead: 42,
SecretScanningWrite: 43,
SecretsRead: 44,
SecretsWrite: 45,
VariablesRead: 46,
VariablesWrite: 47,
WebhooksRead: 48,
WebhooksWrite: 49,
WorkflowsRead: 50,
WorkflowsWrite: 51,
BlockUserRead: 52,
BlockUserWrite: 53,
CodespaceUserSecretsRead: 54,
CodespaceUserSecretsWrite: 55,
EmailRead: 56,
EmailWrite: 57,
FollowersRead: 58,
FollowersWrite: 59,
GpgKeysRead: 60,
GpgKeysWrite: 61,
GistsRead: 62,
GistsWrite: 63,
GitKeysRead: 64,
GitKeysWrite: 65,
LimitsRead: 66,
LimitsWrite: 67,
PlanRead: 68,
PlanWrite: 69,
PrivateInvitesRead: 70,
PrivateInvitesWrite: 71,
ProfileRead: 72,
ProfileWrite: 73,
SigningKeysRead: 74,
SigningKeysWrite: 75,
StarringRead: 76,
StarringWrite: 77,
WatchingRead: 78,
WatchingWrite: 79,
}
IdToPermission = map[int]Permission{
1: NoAccess,
2: ActionsRead,
3: ActionsWrite,
4: AdministrationRead,
5: AdministrationWrite,
6: CodeScanningAlertsRead,
7: CodeScanningAlertsWrite,
8: CodespacesRead,
9: CodespacesWrite,
10: CodespacesLifecycleRead,
11: CodespacesLifecycleWrite,
12: CodespacesMetadataRead,
13: CodespacesMetadataWrite,
14: CodespacesSecretsRead,
15: CodespacesSecretsWrite,
16: CommitStatusesRead,
17: CommitStatusesWrite,
18: ContentsRead,
19: ContentsWrite,
20: CustomPropertiesRead,
21: CustomPropertiesWrite,
22: DependabotAlertsRead,
23: DependabotAlertsWrite,
24: DependabotSecretsRead,
25: DependabotSecretsWrite,
26: DeploymentsRead,
27: DeploymentsWrite,
28: EnvironmentsRead,
29: EnvironmentsWrite,
30: IssuesRead,
31: IssuesWrite,
32: MergeQueuesRead,
33: MergeQueuesWrite,
34: MetadataRead,
35: MetadataWrite,
36: PagesRead,
37: PagesWrite,
38: PullRequestsRead,
39: PullRequestsWrite,
40: RepoSecurityRead,
41: RepoSecurityWrite,
42: SecretScanningRead,
43: SecretScanningWrite,
44: SecretsRead,
45: SecretsWrite,
46: VariablesRead,
47: VariablesWrite,
48: WebhooksRead,
49: WebhooksWrite,
50: WorkflowsRead,
51: WorkflowsWrite,
52: BlockUserRead,
53: BlockUserWrite,
54: CodespaceUserSecretsRead,
55: CodespaceUserSecretsWrite,
56: EmailRead,
57: EmailWrite,
58: FollowersRead,
59: FollowersWrite,
60: GpgKeysRead,
61: GpgKeysWrite,
62: GistsRead,
63: GistsWrite,
64: GitKeysRead,
65: GitKeysWrite,
66: LimitsRead,
67: LimitsWrite,
68: PlanRead,
69: PlanWrite,
70: PrivateInvitesRead,
71: PrivateInvitesWrite,
72: ProfileRead,
73: ProfileWrite,
74: SigningKeysRead,
75: SigningKeysWrite,
76: StarringRead,
77: StarringWrite,
78: WatchingRead,
79: WatchingWrite,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/github/finegrained/finegrained_test.go
================================================
package finegrained
import (
"testing"
"time"
gh "github.com/google/go-github/v67/github"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
analyzerCommon "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
analyzerSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "analyzers1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
wantErr bool
}{
{
name: "finegrained - github-allrepos-actionsRW-contentsRW-issuesRW",
key: analyzerSecrets.MustGetField("GITHUB_FINEGRAINED_ALLREPOS_ACTIONS_RW_CONTENTS_RW_ISSUES_RW"),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &config.Config{}
key := tt.key
client := gh.NewClient(analyzers.NewAnalyzeClient(cfg)).WithAuthToken(key)
md, err := analyzerCommon.GetTokenMetadata(key, client)
if err != nil {
t.Fatalf("could not get token metadata: %s", err)
}
_, err = AnalyzeFineGrainedToken(client, md, cfg.Shallow)
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/github/github.go
================================================
package github
import (
"fmt"
"strings"
"time"
"github.com/fatih/color"
gh "github.com/google/go-github/v67/github"
"golang.org/x/time/rate"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github/classic"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github/finegrained"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
// According to GitHub's rate limiting documentation, the default rate limit for
// authenticated requests (PAT) is 5000 requests per hour. This equates to roughly 1.39
// requests per second. To provide some buffer, we set the rate limit to 1.25
// requests per second with a burst of 10.
// https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28
var rateLimiter = rate.NewLimiter(rate.Limit(1.25), 10)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeGitHub }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
info, err := AnalyzePermissions(a.Cfg, credInfo["key"])
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *common.SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := &analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeGitHub,
Metadata: map[string]any{
"owner": info.Metadata.User.Login,
"type": info.Metadata.Type,
"expiration": info.Metadata.Expiration,
},
}
result.Bindings = append(result.Bindings, secretInfoToUserBindings(info)...)
result.Bindings = append(result.Bindings, secretInfoToRepoBindings(info)...)
result.Bindings = append(result.Bindings, secretInfoToGistBindings(info)...)
for _, repo := range append(info.Repos, info.AccessibleRepos...) {
if repo.Owner.GetType() != "Organization" {
continue
}
name := repo.Owner.GetName()
if name == "" {
continue
}
result.UnboundedResources = append(result.UnboundedResources, analyzers.Resource{
Name: name,
FullyQualifiedName: fmt.Sprintf("github.com/%s", name),
Type: "organization",
})
}
// TODO: Unbound resources
// - Repo owners
// - Gist owners
return result
}
func secretInfoToUserBindings(info *common.SecretInfo) []analyzers.Binding {
return analyzers.BindAllPermissions(*userToResource(info.Metadata.User), info.Metadata.OauthScopes...)
}
func userToResource(user *gh.User) *analyzers.Resource {
name := *user.Login
return &analyzers.Resource{
Name: name,
FullyQualifiedName: fmt.Sprintf("github.com/%s", name),
Type: strings.ToLower(*user.Type), // "user" or "organization"
}
}
func secretInfoToRepoBindings(info *common.SecretInfo) []analyzers.Binding {
var perms []analyzers.Permission
switch info.Metadata.Type {
case common.TokenTypeClassicPAT:
perms = info.Metadata.OauthScopes
case common.TokenTypeFineGrainedPAT:
fineGrainedPermissions := info.RepoAccessMap.([]finegrained.Permission)
for _, perm := range fineGrainedPermissions {
permName, _ := perm.ToString()
perms = append(perms, analyzers.Permission{Value: permName})
}
default:
if len(info.Metadata.OauthScopes) > 0 {
perms = info.Metadata.OauthScopes
}
}
repos := info.Repos
if len(info.AccessibleRepos) > 0 {
repos = info.AccessibleRepos
}
var bindings []analyzers.Binding
for _, repo := range repos {
resource := analyzers.Resource{
Name: *repo.Name,
FullyQualifiedName: fmt.Sprintf("github.com/%s", *repo.FullName),
Type: "repository",
Parent: userToResource(repo.Owner),
}
bindings = append(bindings, analyzers.BindAllPermissions(resource, perms...)...)
}
return bindings
}
func secretInfoToGistBindings(info *common.SecretInfo) []analyzers.Binding {
var bindings []analyzers.Binding
for _, gist := range info.Gists {
resource := analyzers.Resource{
Name: *gist.Description,
FullyQualifiedName: fmt.Sprintf("gist.github.com/%s/%s", *gist.Owner.Login, *gist.ID),
Type: "gist",
Parent: userToResource(gist.Owner),
}
bindings = append(bindings, analyzers.BindAllPermissions(resource, info.Metadata.OauthScopes...)...)
}
return bindings
}
func AnalyzePermissions(cfg *config.Config, key string) (*common.SecretInfo, error) {
if cfg == nil {
cfg = &config.Config{}
}
client := gh.NewClient(analyzers.NewAnalyzeClient(cfg, analyzers.WithRateLimiter(rateLimiter))).WithAuthToken(key)
md, err := common.GetTokenMetadata(key, client)
if err != nil {
return nil, err
}
if md.FineGrained {
return finegrained.AnalyzeFineGrainedToken(client, md, cfg.Shallow)
} else {
return classic.AnalyzeClassicToken(client, md)
}
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] %s", err.Error())
return
}
color.Yellow("[i] Token User: %v", *info.Metadata.User.Login)
if expiry := info.Metadata.Expiration; expiry.IsZero() {
color.Red("[i] Token Expiration: does not expire")
} else {
timeRemaining := time.Until(expiry)
color.Yellow("[i] Token Expiration: %v (%s remaining)", expiry, roughHumanReadableDuration(timeRemaining))
}
color.Yellow("[i] Token Type: %s\n\n", info.Metadata.Type)
if info.Metadata.FineGrained {
finegrained.PrintFineGrainedToken(cfg, info)
return
}
classic.PrintClassicToken(cfg, info)
}
// roughHumanReadableDuration converts a duration into a rough estimate for
// human consumption. The larger the duration, the larger granularity is
// returned.
func roughHumanReadableDuration(d time.Duration) string {
var gran time.Duration
var unit string
switch {
case d < 1*time.Minute:
gran = time.Second
unit = "second"
case d < 1*time.Hour:
gran = time.Minute
unit = "minute"
case d < 24*time.Hour:
gran = time.Hour
unit = "hour"
case d < 4*7*24*time.Hour:
gran = 24 * time.Hour
unit = "day"
case d < 3*4*7*24*time.Hour:
gran = 7 * 24 * time.Hour
unit = "week"
case d < 5*365*24*time.Hour:
gran = 365 * 24 * time.Hour
unit = "month"
default:
gran = 365 * 24 * time.Hour
unit = "year"
}
num := d.Round(gran) / gran
if num != 1 {
unit += "s"
}
return fmt.Sprintf("%d %s", num, unit)
}
================================================
FILE: pkg/analyzer/analyzers/github/github_test.go
================================================
package github
import (
"encoding/json"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
analyzerSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "analyzers1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "finegrained - github-allrepos-actionsRW-contentsRW-issuesRW",
key: analyzerSecrets.MustGetField("GITHUB_FINEGRAINED_ALLREPOS_ACTIONS_RW_CONTENTS_RW_ISSUES_RW"),
wantErr: false,
want: `{
"AnalyzerType": 7,
"Bindings": [
{
"Resource": {
"Name": "private",
"FullyQualifiedName": "github.com/sirdetectsalot/private",
"Type": "repository",
"Metadata": null,
"Parent": {
"Name": "sirdetectsalot",
"FullyQualifiedName": "github.com/sirdetectsalot",
"Type": "user",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "actions:write",
"Parent": null
}
},
{
"Resource": {
"Name": "private",
"FullyQualifiedName": "github.com/sirdetectsalot/private",
"Type": "repository",
"Metadata": null,
"Parent": {
"Name": "sirdetectsalot",
"FullyQualifiedName": "github.com/sirdetectsalot",
"Type": "user",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "contents:write",
"Parent": null
}
},
{
"Resource": {
"Name": "private",
"FullyQualifiedName": "github.com/sirdetectsalot/private",
"Type": "repository",
"Metadata": null,
"Parent": {
"Name": "sirdetectsalot",
"FullyQualifiedName": "github.com/sirdetectsalot",
"Type": "user",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "deployments:read",
"Parent": null
}
},
{
"Resource": {
"Name": "private",
"FullyQualifiedName": "github.com/sirdetectsalot/private",
"Type": "repository",
"Metadata": null,
"Parent": {
"Name": "sirdetectsalot",
"FullyQualifiedName": "github.com/sirdetectsalot",
"Type": "user",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "issues:write",
"Parent": null
}
},
{
"Resource": {
"Name": "private",
"FullyQualifiedName": "github.com/sirdetectsalot/private",
"Type": "repository",
"Metadata": null,
"Parent": {
"Name": "sirdetectsalot",
"FullyQualifiedName": "github.com/sirdetectsalot",
"Type": "user",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "metadata:read",
"Parent": null
}
},
{
"Resource": {
"Name": "public",
"FullyQualifiedName": "github.com/sirdetectsalot/public",
"Type": "repository",
"Metadata": null,
"Parent": {
"Name": "sirdetectsalot",
"FullyQualifiedName": "github.com/sirdetectsalot",
"Type": "user",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "actions:write",
"Parent": null
}
},
{
"Resource": {
"Name": "public",
"FullyQualifiedName": "github.com/sirdetectsalot/public",
"Type": "repository",
"Metadata": null,
"Parent": {
"Name": "sirdetectsalot",
"FullyQualifiedName": "github.com/sirdetectsalot",
"Type": "user",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "contents:write",
"Parent": null
}
},
{
"Resource": {
"Name": "public",
"FullyQualifiedName": "github.com/sirdetectsalot/public",
"Type": "repository",
"Metadata": null,
"Parent": {
"Name": "sirdetectsalot",
"FullyQualifiedName": "github.com/sirdetectsalot",
"Type": "user",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "deployments:read",
"Parent": null
}
},
{
"Resource": {
"Name": "public",
"FullyQualifiedName": "github.com/sirdetectsalot/public",
"Type": "repository",
"Metadata": null,
"Parent": {
"Name": "sirdetectsalot",
"FullyQualifiedName": "github.com/sirdetectsalot",
"Type": "user",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "issues:write",
"Parent": null
}
},
{
"Resource": {
"Name": "public",
"FullyQualifiedName": "github.com/sirdetectsalot/public",
"Type": "repository",
"Metadata": null,
"Parent": {
"Name": "sirdetectsalot",
"FullyQualifiedName": "github.com/sirdetectsalot",
"Type": "user",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "metadata:read",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": {
"owner": "sirdetectsalot",
"expiration": "2026-03-24T15:27:38+05:00",
"type": "Fine-Grained GitHub Personal Access Token"
}
}`,
},
{
name: "v2 ghp",
key: testSecrets.MustGetField("GITHUB_VERIFIED_GHP"),
want: `{
"AnalyzerType": 7,
"Bindings": [
{
"Resource": {
"Name": "truffle-sandbox",
"FullyQualifiedName": "github.com/truffle-sandbox",
"Type": "user",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "notifications",
"AccessLevel": "",
"Parent": null
}
},
{
"Resource": {
"Name": "public gist",
"FullyQualifiedName": "gist.github.com/truffle-sandbox/fecf272c606ddbc5f8486f9c44821312",
"Type": "gist",
"Metadata": null,
"Parent": {
"Name": "truffle-sandbox",
"FullyQualifiedName": "github.com/truffle-sandbox",
"Type": "user",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "notifications",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": {
"owner": "truffle-sandbox",
"expiration": "0001-01-01T00:00:00Z",
"type": "Classic GitHub Personal Access Token"
}
}`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON with indentation
wantJSON, err := json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings and show diff if they don't match
if string(gotJSON) != string(wantJSON) {
diff := cmp.Diff(string(wantJSON), string(gotJSON))
t.Errorf("Analyzer.Analyze() mismatch (-want +got):\n%s", diff)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/gitlab/expected_output.json
================================================
{"AnalyzerType":5,"Bindings":[{"Resource":{"Name":"gitlab.com/user/22466472","FullyQualifiedName":"gitlab.com/user/22466472","Type":"user","Metadata":{"token_created_at":"2024-08-15T06:33:00.337Z","token_expires_at":"2025-08-15","token_id":10470457,"token_name":"test-project-token","token_revoked":false},"Parent":null},"Permission":{"Value":"read_api","Parent":null}},{"Resource":{"Name":"gitlab.com/user/22466472","FullyQualifiedName":"gitlab.com/user/22466472","Type":"user","Metadata":{"token_created_at":"2024-08-15T06:33:00.337Z","token_expires_at":"2025-08-15","token_id":10470457,"token_name":"test-project-token","token_revoked":false},"Parent":null},"Permission":{"Value":"read_repository","Parent":null}},{"Resource":{"Name":"truffletester / trufflehog","FullyQualifiedName":"gitlab.com/project/60871295","Type":"project","Metadata":null,"Parent":null},"Permission":{"Value":"Developer","Parent":null}}],"UnboundedResources":null,"Metadata":{"enterprise":true}}
================================================
FILE: pkg/analyzer/analyzers/gitlab/gitlab.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go gitlab
package gitlab
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
const (
DefaultGitLabHost = "https://gitlab.com"
)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeGitLab }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, errors.New("key not found in credentialInfo")
}
host, ok := credInfo["host"]
if !ok {
host = DefaultGitLabHost
}
info, err := AnalyzePermissions(a.Cfg, key, host)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeGitLab,
Metadata: map[string]any{
"enterprise": info.Metadata.Enterprise,
},
Bindings: []analyzers.Binding{},
}
// Add user and it's permissions to bindings
userFullyQualifiedName := fmt.Sprintf("gitlab.com/user/%d", info.AccessToken.UserID)
userResource := analyzers.Resource{
Name: userFullyQualifiedName,
FullyQualifiedName: userFullyQualifiedName,
Type: "user",
Metadata: map[string]any{
"token_name": info.AccessToken.Name,
"token_id": info.AccessToken.ID,
"token_created_at": info.AccessToken.CreatedAt,
"token_revoked": info.AccessToken.Revoked,
"token_expires_at": info.AccessToken.ExpiresAt,
},
}
for _, scope := range info.AccessToken.Scopes {
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: userResource,
Permission: analyzers.Permission{
Value: scope,
},
})
}
// append project and it's permissions to bindings
for _, project := range info.Projects {
projectResource := analyzers.Resource{
Name: project.NameWithNamespace,
FullyQualifiedName: fmt.Sprintf("gitlab.com/project/%d", project.ID),
Type: "project",
}
accessLevel, ok := access_level_map[project.Permissions.ProjectAccess.AccessLevel]
if !ok {
continue
}
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: projectResource,
Permission: analyzers.Permission{
Value: accessLevel,
},
})
}
return &result
}
// consider calling /api/v4/metadata to learn about gitlab instance version and whether neterrprises is enabled
// we'll call /api/v4/personal_access_tokens and then filter down to scopes.
type AccessTokenJSON struct {
ID int `json:"id"`
Name string `json:"name"`
Revoked bool `json:"revoked"`
CreatedAt string `json:"created_at"`
Scopes []string `json:"scopes"`
LastUsedAt string `json:"last_used_at"`
ExpiresAt string `json:"expires_at"`
UserID int `json:"user_id"`
}
type ProjectsJSON struct {
ID int `json:"id"`
NameWithNamespace string `json:"name_with_namespace"`
Permissions struct {
ProjectAccess struct {
AccessLevel int `json:"access_level"`
} `json:"project_access"`
} `json:"permissions"`
}
type ErrorJSON struct {
Error string `json:"error"`
Scope string `json:"scope"`
}
type MetadataJSON struct {
Version string `json:"version"`
Enterprise bool `json:"enterprise"`
}
func getPersonalAccessToken(cfg *config.Config, key, host string) (AccessTokenJSON, int, error) {
var tokens AccessTokenJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/v4/personal_access_tokens/self", host), nil)
if err != nil {
return tokens, -1, err
}
req.Header.Set("Private-Token", key)
resp, err := client.Do(req)
if err != nil {
return tokens, resp.StatusCode, err
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&tokens); err != nil {
return tokens, resp.StatusCode, err
}
return tokens, resp.StatusCode, nil
}
func getAccessibleProjects(cfg *config.Config, key, host string) ([]ProjectsJSON, error) {
var projects []ProjectsJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/v4/projects", host), nil)
if err != nil {
return projects, err
}
req.Header.Set("Private-Token", key)
// Add query parameters
q := req.URL.Query()
q.Add("min_access_level", "10")
req.URL.RawQuery = q.Encode()
resp, err := client.Do(req)
if err != nil {
return projects, err
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return projects, err
}
newBody := func() io.ReadCloser {
return io.NopCloser(bytes.NewReader(bodyBytes))
}
if err := json.NewDecoder(newBody()).Decode(&projects); err != nil {
var e ErrorJSON
if err := json.NewDecoder(newBody()).Decode(&e); err == nil {
return projects, fmt.Errorf("Insufficient Scope to query for projects. We need api or read_api permissions.")
}
return projects, err
}
return projects, nil
}
func getMetadata(cfg *config.Config, key, host string) (MetadataJSON, error) {
var metadata MetadataJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/v4/metadata", host), nil)
if err != nil {
return metadata, err
}
req.Header.Set("Private-Token", key)
resp, err := client.Do(req)
if err != nil {
return metadata, err
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return metadata, err
}
newBody := func() io.ReadCloser {
return io.NopCloser(bytes.NewReader(bodyBytes))
}
if err := json.NewDecoder(newBody()).Decode(&metadata); err != nil {
return metadata, err
}
if metadata.Version == "" {
var e ErrorJSON
if err := json.NewDecoder(newBody()).Decode(&e); err != nil {
return metadata, err
}
return metadata, fmt.Errorf("Insufficient Scope to query for metadata. We need read_user, ai_features, api or read_api permissions.")
}
return metadata, nil
}
type SecretInfo struct {
AccessToken AccessTokenJSON
Metadata MetadataJSON
Projects []ProjectsJSON
}
func AnalyzePermissions(cfg *config.Config, key string, host string) (*SecretInfo, error) {
// get personal_access_tokens accessible
token, statusCode, err := getPersonalAccessToken(cfg, key, host)
if err != nil {
return nil, err
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("Invalid GitLab Access Token")
}
meta, err := getMetadata(cfg, key, host)
if err != nil {
return nil, err
}
projects, err := getAccessibleProjects(cfg, key, host)
if err != nil {
return nil, err
}
return &SecretInfo{
AccessToken: token,
Metadata: meta,
Projects: projects,
}, nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key, DefaultGitLabHost)
if err != nil {
color.Red("[x] Error: %s", err)
return
}
// print token info
printTokenInfo(info.AccessToken)
// print gitlab instance metadata
if info.Metadata.Version != "" {
printMetadata(info.Metadata)
}
// print token permissions
printTokenPermissions(info.AccessToken)
// print repos accessible
if len(info.Projects) > 0 {
printProjects(info.Projects)
}
}
func getRemainingTime(t string) string {
targetTime, err := time.Parse("2006-01-02", t)
if err != nil {
return ""
}
// Get the current time
currentTime := time.Now()
// Calculate the duration until the target time
durationUntilTarget := targetTime.Sub(currentTime)
durationUntilTarget = durationUntilTarget.Truncate(time.Minute)
// Print the duration
return fmt.Sprintf("%v", durationUntilTarget)
}
func printTokenInfo(token AccessTokenJSON) {
color.Green("[!] Valid GitLab Access Token\n\n")
color.Green("Token Name: %s\n", token.Name)
color.Green("Created At: %s\n", token.CreatedAt)
color.Green("Last Used At: %s\n", token.LastUsedAt)
color.Green("User ID: %d\n", token.UserID)
color.Green("Expires At: %s (%v remaining)\n\n", token.ExpiresAt, getRemainingTime(token.ExpiresAt))
if token.Revoked {
color.Red("Token Revoked: %v\n", token.Revoked)
}
}
func printMetadata(metadata MetadataJSON) {
color.Green("[i] GitLab Instance Metadata\n")
color.Green("Version: %s\n", metadata.Version)
color.Green("Enterprise: %v\n\n", metadata.Enterprise)
}
func printTokenPermissions(token AccessTokenJSON) {
color.Green("[i] Token Permissions\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "Access" /* Add more column headers if needed */})
for _, scope := range token.Scopes {
t.AppendRow([]any{color.GreenString(scope), color.GreenString(gitlab_scopes[scope])})
}
t.SetColumnConfigs([]table.ColumnConfig{
{Number: 2, WidthMax: 100}, // Limit the width of the third column (Description) to 20 characters
})
t.Render()
}
func printProjects(projects []ProjectsJSON) {
color.Green("\n[i] Accessible Projects\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Project", "Access Level" /* Add more column headers if needed */})
for _, project := range projects {
access := access_level_map[project.Permissions.ProjectAccess.AccessLevel]
if project.Permissions.ProjectAccess.AccessLevel == 50 {
access = color.GreenString(access)
} else if project.Permissions.ProjectAccess.AccessLevel >= 30 {
access = color.YellowString(access)
} else {
access = color.RedString(access)
}
t.AppendRow([]any{color.GreenString(project.NameWithNamespace), access})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/gitlab/gitlab_test.go
================================================
package gitlab
import (
_ "embed"
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid gitlab access token",
key: testSecrets.MustGetField("GITLABV2"),
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = \n%s", gotIndented)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/gitlab/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package gitlab
import "errors"
type Permission int
const (
Invalid Permission = iota
Api Permission = iota
ReadUser Permission = iota
ReadApi Permission = iota
ReadRepository Permission = iota
WriteRepository Permission = iota
ReadRegistry Permission = iota
WriteRegistry Permission = iota
Sudo Permission = iota
AdminMode Permission = iota
CreateRunner Permission = iota
ManageRunner Permission = iota
AiFeatures Permission = iota
K8sProxy Permission = iota
ReadServicePing Permission = iota
)
var (
PermissionStrings = map[Permission]string{
Api: "api",
ReadUser: "read_user",
ReadApi: "read_api",
ReadRepository: "read_repository",
WriteRepository: "write_repository",
ReadRegistry: "read_registry",
WriteRegistry: "write_registry",
Sudo: "sudo",
AdminMode: "admin_mode",
CreateRunner: "create_runner",
ManageRunner: "manage_runner",
AiFeatures: "ai_features",
K8sProxy: "k8s_proxy",
ReadServicePing: "read_service_ping",
}
StringToPermission = map[string]Permission{
"api": Api,
"read_user": ReadUser,
"read_api": ReadApi,
"read_repository": ReadRepository,
"write_repository": WriteRepository,
"read_registry": ReadRegistry,
"write_registry": WriteRegistry,
"sudo": Sudo,
"admin_mode": AdminMode,
"create_runner": CreateRunner,
"manage_runner": ManageRunner,
"ai_features": AiFeatures,
"k8s_proxy": K8sProxy,
"read_service_ping": ReadServicePing,
}
PermissionIDs = map[Permission]int{
Api: 1,
ReadUser: 2,
ReadApi: 3,
ReadRepository: 4,
WriteRepository: 5,
ReadRegistry: 6,
WriteRegistry: 7,
Sudo: 8,
AdminMode: 9,
CreateRunner: 10,
ManageRunner: 11,
AiFeatures: 12,
K8sProxy: 13,
ReadServicePing: 14,
}
IdToPermission = map[int]Permission{
1: Api,
2: ReadUser,
3: ReadApi,
4: ReadRepository,
5: WriteRepository,
6: ReadRegistry,
7: WriteRegistry,
8: Sudo,
9: AdminMode,
10: CreateRunner,
11: ManageRunner,
12: AiFeatures,
13: K8sProxy,
14: ReadServicePing,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/gitlab/permissions.yaml
================================================
permissions:
- api
- read_user
- read_api
- read_repository
- write_repository
- read_registry
- write_registry
- sudo
- admin_mode
- create_runner
- manage_runner
- ai_features
- k8s_proxy
- read_service_ping
================================================
FILE: pkg/analyzer/analyzers/gitlab/scopes.go
================================================
package gitlab
var gitlab_scopes = map[string]string{
"api": "Grants complete read/write access to the API, including all groups and projects, the container registry, the dependency proxy, and the package registry. Also grants complete read/write access to the registry and repository using Git over HTTP.",
"read_user": "Grants read-only access to the authenticated user’s profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users.",
"read_api": "Grants read access to the API, including all groups and projects, the container registry, and the package registry.",
"read_repository": "Grants read-only access to repositories on private projects using Git-over-HTTP or the Repository Files API.",
"write_repository": "Grants read-write access to repositories on private projects using Git-over-HTTP (not using the API).",
"read_registry": "Grants read-only (pull) access to container registry images if a project is private and authorization is required. Available only when the container registry is enabled.",
"write_registry": "Grants read-write (push) access to container registry images if a project is private and authorization is required. Available only when the container registry is enabled.",
"sudo": "Grants permission to perform API actions as any user in the system, when authenticated as an administrator.",
"admin_mode": "Grants permission to perform API actions as an administrator, when Admin Mode is enabled. (Introduced in GitLab 15.8.)",
"create_runner": "Grants permission to create runners.",
"manage_runner": "Grants permission to manage runners.",
"ai_features": "Grants permission to perform API actions for GitLab Duo. This scope is designed to work with the GitLab Duo Plugin for JetBrains. For all other extensions, see scope requirements.",
"k8s_proxy": "Grants permission to perform Kubernetes API calls using the agent for Kubernetes.",
"read_service_ping": "Grant access to download Service Ping payload through the API when authenticated as an admin use. (Introduced in GitLab 16.8.",
}
var access_level_map = map[int]string{
0: "No access",
5: "Minimal access",
10: "Guest",
20: "Reporter",
30: "Developer",
40: "Maintainer",
50: "Owner",
}
================================================
FILE: pkg/analyzer/analyzers/groq/groq.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go groq
package groq
import (
"errors"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
// SecretInfo hold the information about the groq key
type SecretInfo struct {
Valid bool
Reference string
GroqResources []GroqResource
Permissions []string
Misc map[string]string
}
// GroqResource is a single groq resource which can be accessed with groq api key
type GroqResource struct {
ID string
Name string
Type string
Permission string
Metadata map[string]string
}
// appendGroqResource append the single groq resource to secretinfo groqresources list
func (s *SecretInfo) appendGroqResource(resource GroqResource) {
s.GroqResources = append(s.GroqResources, resource)
}
// updateMetadata safely update the metadata of the groq resource
func (g GroqResource) updateMetadata(key, value string) {
if g.Metadata == nil {
g.Metadata = map[string]string{}
}
g.Metadata[key] = value
}
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeGroq
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, exist := credInfo["key"]
if !exist {
return nil, errors.New("key not found in credentials info")
}
secretInfo, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(secretInfo), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
// just print the error in cli and continue as a partial success
color.Red("[x] Invalid Groq API key\n")
color.Red("[x] Error : %s", err.Error())
return
}
if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}
color.Green("[i] Valid Groq API key\n")
color.Yellow("\n[i] Permission: Full Access\n")
if len(info.GroqResources) > 0 {
printGroqResources(info.GroqResources)
}
color.Yellow("\n[!] Expires: Never")
}
func AnalyzePermissions(cfg *config.Config, apiKey string) (*SecretInfo, error) {
// create a HTTP client
client := analyzers.NewAnalyzeClient(cfg)
var secretInfo = &SecretInfo{Valid: true}
if err := captureBatches(client, apiKey, secretInfo); err != nil {
return nil, err
}
if err := captureFiles(client, apiKey, secretInfo); err != nil {
return nil, err
}
return secretInfo, nil
}
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeGroq,
Metadata: map[string]any{"Valid_Key": info.Valid},
Bindings: make([]analyzers.Binding, len(info.GroqResources)),
}
// extract information to create bindings and append to result bindings
for _, groqResource := range info.GroqResources {
binding := analyzers.Binding{
Resource: analyzers.Resource{
Name: groqResource.Name,
FullyQualifiedName: groqResource.ID,
Type: groqResource.Type,
Metadata: map[string]any{},
},
Permission: analyzers.Permission{
Value: groqResource.Permission,
},
}
for key, value := range groqResource.Metadata {
binding.Resource.Metadata[key] = value
}
result.Bindings = append(result.Bindings, binding)
}
return &result
}
func printGroqResources(resources []GroqResource) {
color.Green("\n[i] Resources:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Name", "Type"})
for _, resource := range resources {
t.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/groq/groq_test.go
================================================
package groq
import (
_ "embed"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*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)
}
apiKey := testSecrets.MustGetField("GROQ")
tests := []struct {
name string
apiKey string
want string
wantErr bool
}{
{
name: "valid dockerhub credentials",
apiKey: apiKey,
want: `{"AnalyzerType":2,"Bindings":[],"UnboundedResources":null,"Metadata":{"Valid_Key":true}}`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.apiKey})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
fmt.Println(string(gotJSON))
// compare the JSON strings
if string(gotJSON) != string(tt.want) {
// pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(tt.want, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/groq/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package groq
import "errors"
type Permission int
const (
Invalid Permission = iota
FullAccess Permission = iota
)
var (
PermissionStrings = map[Permission]string{
FullAccess: "full_access",
}
StringToPermission = map[string]Permission{
"full_access": FullAccess,
}
PermissionIDs = map[Permission]int{
FullAccess: 1,
}
IdToPermission = map[int]Permission{
1: FullAccess,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/groq/permissions.yaml
================================================
permissions:
- full_access # by default groq api key has full access
================================================
FILE: pkg/analyzer/analyzers/groq/requests.go
================================================
package groq
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
var (
permissionErr = "permissions_error"
notAvailableForPlan = "not_available_for_plan"
)
// errorResponse is the response from groq APIs in case of any error
type errorResponse struct {
Error struct {
Message string `json:"message"`
Type string `json:"type"`
Code string `json:"code"`
} `json:"error"`
}
// listBatchesResponse is the response of /v1/batches API
type listBatchesResponse struct {
Data []batch `json:"data"`
}
// batch represent a single batch inside batches list
type batch struct {
ID string `json:"id"`
Object string `json:"object"`
Endpoint string `json:"endpoint"`
InputFileID string `json:"input_file_id"`
Status string `json:"status"`
ExpiresAt int64 `json:"expires_at"`
}
// listBatchesResponse is the response of /v1/files API
type listFilesResponse struct {
Data []file `json:"data"`
}
// file represents a single file object inside files list
type file struct {
ID string `json:"id"`
Object string `json:"object"`
CreatedAt int64 `json:"created_at"`
Filename string `json:"filename"`
Purpose string `json:"purpose"`
}
func isPermissionError(err errorResponse) bool {
// has permissions error or not available for the plan subscribed
if err.Error.Type == permissionErr && err.Error.Code == notAvailableForPlan {
return true
}
return false
}
// makeGroqRequest send the API request to passed url with passed key as API Key and return response body and status code
func makeGroqRequest(client *http.Client, url, key string) ([]byte, int, error) {
// create request
req, err := http.NewRequest(http.MethodGet, url, http.NoBody)
if err != nil {
return nil, 0, err
}
// add required keys in the header
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, 0, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
responseBodyByte, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, err
}
return responseBodyByte, resp.StatusCode, nil
}
// docs: https://console.groq.com/docs/api-reference#batches-list
func captureBatches(client *http.Client, key string, secretInfo *SecretInfo) error {
response, statusCode, err := makeGroqRequest(client, "https://api.groq.com/openai/v1/batches", key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var batches listBatchesResponse
if err := json.Unmarshal(response, &batches); err != nil {
return err
}
for _, batch := range batches.Data {
resource := GroqResource{
ID: batch.ID,
Name: batch.ID, // no specific name for batch
Type: batch.Object,
Permission: PermissionStrings[FullAccess],
}
resource.updateMetadata("status", batch.Status)
resource.updateMetadata("endpoint", batch.Endpoint)
resource.updateMetadata("input file id", batch.InputFileID)
resource.updateMetadata("expires at", time.Unix(batch.ExpiresAt, 0).UTC().Format("2006-01-02 15:04:05 UTC"))
secretInfo.appendGroqResource(resource)
}
return nil
case http.StatusForbidden:
var errResp errorResponse
if err := json.Unmarshal(response, &errResp); err != nil {
return err
}
if isPermissionError(errResp) {
return nil
}
return fmt.Errorf("unexpected error: %s", errResp.Error.Message)
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// docs: https://console.groq.com/docs/api-reference#files-list
func captureFiles(client *http.Client, key string, secretInfo *SecretInfo) error {
response, statusCode, err := makeGroqRequest(client, "https://api.groq.com/openai/v1/files", key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var files listFilesResponse
if err := json.Unmarshal(response, &files); err != nil {
return err
}
for _, file := range files.Data {
resource := GroqResource{
ID: file.ID,
Name: file.Filename,
Type: file.Object,
Permission: PermissionStrings[FullAccess],
}
resource.updateMetadata("purpose", file.Purpose)
resource.updateMetadata("created at", time.Unix(file.CreatedAt, 0).UTC().Format("2006-01-02 15:04:05 UTC"))
secretInfo.appendGroqResource(resource)
}
return nil
case http.StatusForbidden:
var errResp errorResponse
if err := json.Unmarshal(response, &errResp); err != nil {
return err
}
if isPermissionError(errResp) {
return nil
}
return fmt.Errorf("unexpected error: %s", errResp.Error.Message)
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
================================================
FILE: pkg/analyzer/analyzers/huggingface/huggingface.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go huggingface
package huggingface
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
const (
FINEGRAINED = "fineGrained"
WRITE = "write"
READ = "read"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeHuggingFace }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok || key == "" {
return nil, fmt.Errorf("key not found in credentialInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func bakeUnboundedResources(tokenJSON HFTokenJSON) []analyzers.Resource {
unboundedResources := make([]analyzers.Resource, len(tokenJSON.Orgs))
for idx, org := range tokenJSON.Orgs {
unboundedResources[idx] = analyzers.Resource{
Name: org.Name,
FullyQualifiedName: "huggingface.com/user/" + tokenJSON.Username + "/organization/" + org.Name,
Type: "organization",
Metadata: map[string]interface{}{
"role": org.Role,
"is_enterprise": org.IsEnterprise,
},
}
}
return unboundedResources
}
func bakeUnfineGrainedBindings(allModels []Model, tokenJSON HFTokenJSON) []analyzers.Binding {
bindings := make([]analyzers.Binding, len(allModels))
for idx, model := range allModels {
// Add Read Privs to All Models
modelResource := analyzers.Resource{
Name: model.Name,
FullyQualifiedName: "huggingface.com/model/" + model.ID,
Type: "model",
Metadata: map[string]interface{}{
"private": model.Private,
},
}
// means both read & write permission for the model
accessLevel := string(analyzers.READ)
if tokenJSON.Auth.AccessToken.Type == WRITE {
accessLevel = string(analyzers.WRITE)
}
bindings[idx] = analyzers.Binding{
Resource: modelResource,
Permission: analyzers.Permission{
Value: string(accessLevel),
},
}
}
return bindings
}
// finegrained scopes are grouped by org, user or model.
func bakefineGrainedBindings(allModels []Model, tokenJSON HFTokenJSON) []analyzers.Binding {
// this section will extract the relevant permissions for each entity and store them in a map
var nameToPermissions = make(map[string]analyzers.Permission)
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
privs := analyzers.Permission{
Value: string(analyzers.NONE),
}
for _, perm := range permission.Permissions {
if perm == "repo.content.read" {
privs.Value = string(analyzers.READ)
} else if perm == "repo.write" {
privs.Value = string(analyzers.WRITE)
}
}
if permission.Entity.Type == "user" || permission.Entity.Type == "org" {
nameToPermissions[permission.Entity.Name] = privs
} else if permission.Entity.Type == "model" {
nameToPermissions[modelNameLookup(allModels, permission.Entity.ID)] = privs
}
}
bindings := make([]analyzers.Binding, len(allModels))
for idx, model := range allModels {
// Add Read Privs to All Models
modelResource := analyzers.Resource{
Name: model.Name,
FullyQualifiedName: "huggingface.com/model/" + model.ID,
Type: "model",
Metadata: map[string]interface{}{
"private": model.Private,
},
}
var perm analyzers.Permission
// get username/orgname for each model and apply those permissions
modelUsername := strings.Split(model.Name, "/")[0]
if permissions, ok := nameToPermissions[modelUsername]; ok {
perm = permissions
}
// override model permissions with repo-specific permissions
if permissions, ok := nameToPermissions[model.Name]; ok {
perm = permissions
}
bindings[idx] = analyzers.Binding{
Resource: modelResource,
Permission: perm,
}
}
return bindings
}
func bakeOrganizationBindings(tokenJSON HFTokenJSON) []analyzers.Binding {
// check if there are any org permissions
// if so, save them as a map. Only need to do this once
// even if multiple orgs b/c as of 6/6/24, users can only define one set of scopes
// for all orgs referenced on an access token
orgPermissions := map[string]struct{}{}
var orgResource *analyzers.Resource = nil
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
if permission.Entity.Type == "org" {
orgResource = &analyzers.Resource{
Name: permission.Entity.Name,
FullyQualifiedName: "hugggingface.com/organization/" + permission.Entity.ID,
Type: "organization",
}
for _, perm := range permission.Permissions {
orgPermissions[perm] = struct{}{}
}
break
}
}
bindings := make([]analyzers.Binding, 0)
// check if there are any org permissions
if orgResource == nil {
return bindings
}
for _, permission := range org_scopes_order {
for key, value := range org_scopes[permission] {
if _, ok := orgPermissions[key]; ok {
bindings = append(bindings, analyzers.Binding{
Resource: *orgResource,
Permission: analyzers.Permission{
Value: value,
},
})
}
}
}
return bindings
}
func bakeUserBindings(tokenJSON HFTokenJSON) []analyzers.Binding {
bindings := make([]analyzers.Binding, 0)
// build a map of all user permissions
users := map[string]struct{}{}
userPermissions := map[string]struct{}{}
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
if permission.Entity.Type == "user" {
users[permission.Entity.Name] = struct{}{}
for _, perm := range permission.Permissions {
userPermissions[perm] = struct{}{}
}
}
}
// global permissions only apply to user tokens as of 6/6/24
// but there would be a naming collision in the scopes document
// so we prepend "global." to the key and then add to the map
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Global {
userPermissions["global."+permission] = struct{}{}
}
// check if there are any user permissions
if len(userPermissions) == 0 {
return bindings
}
userResource := analyzers.Resource{
Name: tokenJSON.Name,
FullyQualifiedName: "huggingface.com/user/" + tokenJSON.Username,
Type: "user",
}
for _, permission := range user_scopes_order {
for key, value := range user_scopes[permission] {
if _, ok := userPermissions[key]; ok {
bindings = append(bindings, analyzers.Binding{
Resource: userResource,
Permission: analyzers.Permission{
Value: value,
},
})
}
}
}
return bindings
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeHuggingFace,
Metadata: map[string]interface{}{
"username": info.Token.Username,
"name": info.Token.Name,
"token_name": info.Token.Auth.AccessToken.Name,
"token_type": info.Token.Auth.AccessToken.Type,
},
}
if len(info.Token.Orgs) > 0 {
result.UnboundedResources = bakeUnboundedResources(info.Token)
}
result.Bindings = make([]analyzers.Binding, 0)
if info.Token.Auth.AccessToken.Type == FINEGRAINED {
result.Bindings = append(result.Bindings, bakefineGrainedBindings(info.Models, info.Token)...)
result.Bindings = append(result.Bindings, bakeOrganizationBindings(info.Token)...)
result.Bindings = append(result.Bindings, bakeUserBindings(info.Token)...)
} else {
result.Bindings = append(result.Bindings, bakeUnfineGrainedBindings(info.Models, info.Token)...)
}
return &result
}
// HFTokenJSON is the struct for the HF /whoami-v2 API JSON response
type HFTokenJSON struct {
Username string `json:"name"`
Name string `json:"fullname"`
Orgs []struct {
Name string `json:"name"`
Role string `json:"roleInOrg"`
IsEnterprise bool `json:"isEnterprise"`
} `json:"orgs"`
Auth struct {
AccessToken struct {
Name string `json:"displayName"`
Type string `json:"role"`
CreatedAt string `json:"createdAt"`
FineGrained struct {
Global []string `json:"global"`
Scoped []struct {
Entity struct {
Type string `json:"type"`
Name string `json:"name"`
ID string `json:"_id"`
} `json:"entity"`
Permissions []string `json:"permissions"`
} `json:"scoped"`
} `json:"fineGrained"`
}
} `json:"auth"`
}
type Permissions struct {
Read bool
Write bool
}
type Model struct {
Name string `json:"id"`
ID string `json:"_id"`
Private bool `json:"private"`
Permissions Permissions
}
// getModelsByAuthor calls the HF API /models endpoint with the author query param
// returns a list of models and an error
func getModelsByAuthor(cfg *config.Config, key string, author string) ([]Model, error) {
var modelsJSON []Model
// create a new request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://huggingface.co/api/models", nil)
if err != nil {
return modelsJSON, err
}
// Add bearer token
req.Header.Add("Authorization", "Bearer "+key)
// Add author param
q := req.URL.Query()
q.Add("author", author)
req.URL.RawQuery = q.Encode()
// send the request
resp, err := client.Do(req)
if err != nil {
return modelsJSON, err
}
// defer the response body closing
defer resp.Body.Close()
// read response
if err := json.NewDecoder(resp.Body).Decode(&modelsJSON); err != nil {
return modelsJSON, err
}
return modelsJSON, nil
}
// getTokenInfo calls the HF API /whoami-v2 endpoint to get the token info
// returns the token info, a boolean indicating token validity, and an error
func getTokenInfo(cfg *config.Config, key string) (HFTokenJSON, bool, error) {
var tokenJSON HFTokenJSON
// create a new request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://huggingface.co/api/whoami-v2", nil)
if err != nil {
return tokenJSON, false, err
}
// Add bearer token
req.Header.Add("Authorization", "Bearer "+key)
// send the request
resp, err := client.Do(req)
if err != nil {
return tokenJSON, false, err
}
// check if the response is 200
if resp.StatusCode != 200 {
return tokenJSON, false, nil
}
// defer the response body closing
defer resp.Body.Close()
// read response
if err := json.NewDecoder(resp.Body).Decode(&tokenJSON); err != nil {
return tokenJSON, true, err
}
return tokenJSON, true, nil
}
type SecretInfo struct {
Token HFTokenJSON
Models []Model
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
// get token info
token, success, err := getTokenInfo(cfg, key)
if err != nil {
return nil, err
}
if !success {
return nil, fmt.Errorf("Invalid HuggingFace Access Token")
}
// get all models by username
var allModels []Model
userModels, err := getModelsByAuthor(cfg, key, token.Username)
if err != nil {
return nil, err
}
allModels = append(allModels, userModels...)
// get all models from all orgs
for _, org := range token.Orgs {
orgModels, err := getModelsByAuthor(cfg, key, org.Name)
if err != nil {
return nil, err
}
allModels = append(allModels, orgModels...)
}
return &SecretInfo{
Token: token,
Models: allModels,
}, nil
}
// AnalyzeAndPrintPermissions prints the permissions of a HuggingFace API key
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
color.Green("[!] Valid HuggingFace Access Token\n\n")
// print user info
color.Yellow("[i] Username: " + info.Token.Username)
color.Yellow("[i] Name: " + info.Token.Name)
color.Yellow("[i] Token Name: " + info.Token.Auth.AccessToken.Name)
color.Yellow("[i] Token Type: " + info.Token.Auth.AccessToken.Type)
// print org info
printOrgs(info.Token)
// print accessible models
printAccessibleModels(info.Models, info.Token)
if info.Token.Auth.AccessToken.Type == FINEGRAINED {
// print org permissions
printOrgPermissions(info.Token)
// print user permissions
printUserPermissions(info.Token)
}
}
// printUserPermissions prints the user permissions
// only applies to fine-grained tokens
func printUserPermissions(tokenJSON HFTokenJSON) {
color.Green("\n[i] User Permissions:")
// build a map of all user permissions
userPermissions := map[string]struct{}{}
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
if permission.Entity.Type == "user" {
for _, perm := range permission.Permissions {
userPermissions[perm] = struct{}{}
}
}
}
// global permissions only apply to user tokens as of 6/6/24
// but there would be a naming collision in the scopes document
// so we prepend "global." to the key and then add to the map
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Global {
userPermissions["global."+permission] = struct{}{}
}
// check if there are any user permissions
if len(userPermissions) == 0 {
color.Red("\tNo user permissions scoped.")
return
}
// print the user permissions
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Category", "Permission", "In-Scope"})
for _, permission := range user_scopes_order {
t.AppendRow([]interface{}{permission, "---", "---"})
for key, value := range user_scopes[permission] {
if _, ok := userPermissions[key]; ok {
t.AppendRow([]interface{}{"", color.GreenString(value), color.GreenString("True")})
} else {
t.AppendRow([]interface{}{"", value, "False"})
}
}
}
t.Render()
}
// printOrgPermissions prints the organization permissions
// only applies to fine-grained tokens
func printOrgPermissions(tokenJSON HFTokenJSON) {
color.Green("\n[i] Organization Permissions:")
// check if there are any org permissions
// if so, save them as a map. Only need to do this once
// even if multiple orgs b/c as of 6/6/24, users can only define one set of scopes
// for all orgs referenced on an access token
orgScoped := false
orgPermissions := map[string]struct{}{}
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
if permission.Entity.Type == "org" {
orgScoped = true
for _, perm := range permission.Permissions {
orgPermissions[perm] = struct{}{}
}
break
}
}
// check if there are any org permissions
if !orgScoped {
color.Red("\tNo organization permissions scoped.")
return
}
// print the org permissions
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Category", "Permission", "In-Scope"})
for _, permission := range org_scopes_order {
t.AppendRow([]interface{}{permission, "---", "---"})
for key, value := range org_scopes[permission] {
if _, ok := orgPermissions[key]; ok {
t.AppendRow([]interface{}{"", color.GreenString(value), color.GreenString("True")})
} else {
t.AppendRow([]interface{}{"", value, "False"})
}
}
}
t.Render()
}
// printOrgs prints the organizations the user is a member of
func printOrgs(tokenJSON HFTokenJSON) {
color.Green("\n[i] Organizations:")
if len(tokenJSON.Orgs) == 0 {
color.Yellow("\tNo organizations found.")
return
}
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Name", "Role", "Is Enterprise"})
for _, org := range tokenJSON.Orgs {
enterprise := ""
role := ""
if org.IsEnterprise {
enterprise = color.New(color.FgGreen).Sprint("True")
} else {
enterprise = "False"
}
if org.Role == "admin" {
role = color.New(color.FgGreen).Sprint("Admin")
} else {
role = org.Role
}
t.AppendRow([]interface{}{color.GreenString(org.Name), role, enterprise})
}
t.Render()
}
// modelNameLookup is a helper function to lookup model name by _id
func modelNameLookup(models []Model, id string) string {
for _, model := range models {
if model.ID == id {
return model.Name
}
}
return ""
}
// printAccessibleModels adds permissions as needed to each model
//
// and then calls the printModelsTable function
func printAccessibleModels(allModels []Model, tokenJSON HFTokenJSON) {
color.Green("\n[i] Accessible Models:")
if tokenJSON.Auth.AccessToken.Type != FINEGRAINED {
// Add Read Privs to All Models
for idx := range allModels {
allModels[idx].Permissions.Read = true
}
// Add Write Privs to All Models if Write Access
if tokenJSON.Auth.AccessToken.Type == WRITE {
for idx := range allModels {
allModels[idx].Permissions.Write = true
}
}
// Print Models Table
printModelsTable(allModels)
return
}
// finegrained scopes are grouped by org, user or model.
// this section will extract the relevant permissions for each entity and store them in a map
var nameToPermissions = make(map[string]Permissions)
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
read := false
write := false
for _, perm := range permission.Permissions {
if perm == "repo.content.read" {
read = true
} else if perm == "repo.write" {
write = true
}
}
if permission.Entity.Type == "user" || permission.Entity.Type == "org" {
nameToPermissions[permission.Entity.Name] = Permissions{Read: read, Write: write}
} else if permission.Entity.Type == "model" {
nameToPermissions[modelNameLookup(allModels, permission.Entity.ID)] = Permissions{Read: read, Write: write}
}
}
// apply permissions to all models
for idx := range allModels {
// get username/orgname for each model and apply those permissions
modelUsername := strings.Split(allModels[idx].Name, "/")[0]
if permissions, ok := nameToPermissions[modelUsername]; ok {
allModels[idx].Permissions = permissions
}
// override model permissions with repo-specific permissions
if permissions, ok := nameToPermissions[allModels[idx].Name]; ok {
allModels[idx].Permissions = permissions
}
}
// Print Models Table
printModelsTable(allModels)
}
// printModelsTable prints the models table
func printModelsTable(models []Model) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Model", "Private", "Read", "Write"})
for _, model := range models {
var name, read, write, private string
if model.Permissions.Read {
read = color.New(color.FgGreen).Sprint("True")
} else {
read = "False"
}
if model.Permissions.Write {
write = color.New(color.FgGreen).Sprint("True")
} else {
write = "False"
}
if model.Private {
private = color.New(color.FgGreen).Sprint("True")
name = color.New(color.FgGreen).Sprint(model.Name)
} else {
private = "False"
name = model.Name
}
t.AppendRow([]interface{}{name, private, read, write})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/huggingface/huggingface_test.go
================================================
package huggingface
import (
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
func TestAnalyzer_Analyze(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)
}
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid Huggingface key",
key: testSecrets.MustGetField("HUGGINGFACE"),
want: `{
"AnalyzerType":6,
"Bindings":[
{
"Resource":{
"Name":"zubairkhan/test",
"FullyQualifiedName": "huggingface.com/model/64d8220c0d879296892ab835",
"Type":"model",
"Metadata":{
"private":false
},
"Parent":null
},
"Permission":{
"Value":"Read",
"Parent":null
}
},
{
"Resource":{
"Name":"zubairkhan/first_repo",
"FullyQualifiedName": "huggingface.com/model/64d82349a787c9bc7bbb2ab4",
"Type":"model",
"Metadata":{
"private":true
},
"Parent":null
},
"Permission":{
"Value":"Read",
"Parent":null
}
}
],
"UnboundedResources":null,
"Metadata":{
"name":"Zubair Khan",
"token_name":"another_one",
"token_type":"read",
"username":"zubairkhan"
}
}`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/huggingface/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package huggingface
import "errors"
type Permission int
const (
Invalid Permission = iota
Read Permission = iota
Write Permission = iota
)
var (
PermissionStrings = map[Permission]string{
Read: "read",
Write: "write",
}
StringToPermission = map[string]Permission{
"read": Read,
"write": Write,
}
PermissionIDs = map[Permission]int{
Read: 1,
Write: 2,
}
IdToPermission = map[int]Permission{
1: Read,
2: Write,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/huggingface/permissions.yaml
================================================
permissions:
- read
- write
================================================
FILE: pkg/analyzer/analyzers/huggingface/scopes.go
================================================
package huggingface
//nolint:unused
var repo_scopes = map[string]string{
"repo.content.read": "Read access to contents",
"discussion.write": "Interact with discussions / Open pull requests",
"repo.write": "Write access to contents/settings",
}
var org_scopes_order = []string{
"Repos",
"Collections",
"Inference endpoints",
"Org settings",
}
var org_scopes = map[string]map[string]string{
"Repos": {
"repo.content.read": "Read access to contents of all repos",
"discussion.write": "Interact with discussions / Open pull requests on all repos",
"repo.write": "Write access to contents/settings of all repos",
},
"Collections": {
"collection.read": "Read access to all collections",
"collection.write": "Write access to all collections",
},
"Inference endpoints": {
"inference.endpoints.infer.write": "Make calls to inference endpoints",
"inference.endpoints.write": "Manage inference endpoints",
},
"Org settings": {
"org.read": "Read access to organization's settings",
"org.write": "Write access to organization's settings / member management",
},
}
var user_scopes_order = []string{
"Billing",
"Collections",
"Discussions & Posts",
"Inference",
"Repos",
"Webhooks",
}
var user_scopes = map[string]map[string]string{
"Billing": {
"user.billing.read": "Read access to user's billing usage",
},
"Collections": {
"collection.read": "Read access to all collections under user's namespace",
"collection.write": "Write access to all collections under user's namespace",
},
"Discussions & Posts": {
// Note: prepending global. to scopes that are nested under "global" in fine-grained permissions JSON
// otherwise they would overlap with user scopes under the "scoped" JSON
"discussion.write": "Interact with discussions / Open pull requests on repos under user's namespace",
"global.discussion.write": "Interact with discussions / Open pull requests on external repos",
"global.post.write": "Interact with posts",
},
"Inference": {
"global.inference.serverless.write": "Make calls to the serverless Inference API",
"inference.endpoints.infer.write": "Make calls to inference endpoints",
"inference.endpoints.write": "Manage inference endpoints",
},
"Repos": {
"repo.content.read": "Read access to contents of all repos under user's namespace",
"repo.write": "Write access to contents/settings of all repos under user's namespace",
},
"Webhooks": {
"user.webhooks.read": "Access webhooks data",
"user.webhooks.write": "Create and manage webhooks",
},
}
================================================
FILE: pkg/analyzer/analyzers/jira/jira.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go jira
package jira
import (
"encoding/json"
"fmt"
"os"
"slices"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeJira
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
token, exist := credInfo["token"]
if !exist {
return nil, fmt.Errorf("token not found in credential info")
}
domain, exist := credInfo["domain"]
if !exist {
return nil, fmt.Errorf("domain not found in credential info")
}
email, exist := credInfo["email"]
if !exist {
return nil, fmt.Errorf("email not found in credential info")
}
info, err := AnalyzePermissions(a.Cfg, token, domain, email)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, domain, email, token string) {
info, err := AnalyzePermissions(cfg, token, domain, email)
if err != nil {
// just print the error in cli and continue as a partial success
color.Red("[x] Error : %s", err.Error())
} else {
color.Green("[!] Valid Jira API token\n\n")
}
if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}
printUserInfo(info.UserInfo)
printPermissions(info.Permissions)
printResources(info.Resources)
}
func AnalyzePermissions(cfg *config.Config, token, domain, email string) (*SecretInfo, error) {
// create http client
client := analyzers.NewAnalyzeClient(cfg)
var secretInfo = &SecretInfo{}
// capture the user information
if err := captureUserInfo(client, token, domain, email, secretInfo); err != nil {
return nil, err
}
body, _, err := capturePermissions(client, domain, email, token)
if err != nil {
return secretInfo, fmt.Errorf("failed to check permissions: %w", err)
}
var permissionsResp JiraPermissionsResponse
if err := json.Unmarshal(body, &permissionsResp); err != nil {
return secretInfo, fmt.Errorf("failed to unmarshal permissions response: %w", err)
}
var grantedPermissions []string
for key, perm := range permissionsResp.Permissions {
if perm.HavePermission {
grantedPermissions = append(grantedPermissions, key)
}
}
slices.Sort(grantedPermissions)
secretInfo.Permissions = grantedPermissions
// capture the resources
if err := captureResources(client, domain, email, token, secretInfo, grantedPermissions); err != nil {
// return secretInfo as well in case of error for partial success
return secretInfo, err
}
return secretInfo, nil
}
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeJira,
Metadata: map[string]any{},
Bindings: make([]analyzers.Binding, 0),
}
for _, resource := range info.Resources {
for _, perm := range resource.Permissions {
binding := analyzers.Binding{
Resource: *secretInfoResourceToAnalyzerResource(resource),
Permission: analyzers.Permission{
Value: perm,
},
}
if resource.Parent != nil {
binding.Resource.Parent = secretInfoResourceToAnalyzerResource(*resource.Parent)
}
result.Bindings = append(result.Bindings, binding)
}
}
return &result
}
// secretInfoResourceToAnalyzerResource translate secret info resource to analyzer resource for binding
func secretInfoResourceToAnalyzerResource(resource JiraResource) *analyzers.Resource {
analyzerRes := analyzers.Resource{
// make fully qualified name unique
FullyQualifiedName: resource.Type + "/" + resource.ID,
Name: resource.Name,
Type: resource.Type,
Metadata: map[string]any{},
}
for key, value := range resource.Metadata {
analyzerRes.Metadata[key] = value
}
return &analyzerRes
}
// cli print functions
func printUserInfo(user JiraUser) {
if user.AccountID == "" {
color.Red("[x] No user information found")
return
}
color.Yellow("[i] User Information:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"ID", "Name", "Account Type", "Email", "Active"})
t.AppendRow(table.Row{color.GreenString(user.AccountID), color.GreenString(user.DisplayName), color.GreenString(user.AccountType), color.GreenString(user.EmailAddress), color.GreenString(fmt.Sprintf("%t", user.Active))})
t.Render()
}
func printPermissions(permissions []string) {
if len(permissions) == 0 {
color.Red("[x] No permissions found")
return
}
color.Yellow("[i] Permissions:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Permission"})
for _, scope := range permissions {
t.AppendRow(table.Row{color.GreenString(scope)})
}
t.Render()
}
func printResources(resources []JiraResource) {
if len(resources) == 0 {
color.Red("[x] No resources found")
return
}
color.Yellow("[i] Resources:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Name", "Type"})
for _, resource := range resources {
t.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/jira/jira_test.go
================================================
package jira
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed result_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "analyzers1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
jiraDomain := testSecrets.MustGetField("JIRA_DOMAIN_ANALYZE")
jiraEmail := testSecrets.MustGetField("JIRA_EMAIL_ANALYZE")
jiraToken := testSecrets.MustGetField("JIRA_TOKEN_ANALYZE")
tests := []struct {
name string
domain string
email string
token string
want []byte
wantErr bool
}{
{
name: "valid jira token",
domain: jiraDomain,
email: jiraEmail,
token: jiraToken,
want: expectedOutput,
wantErr: false,
},
{
name: "invalid jira token",
domain: jiraDomain,
email: jiraEmail,
token: "invalid",
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"token": tt.token, "domain": tt.domain, "email": tt.email})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr {
if got != nil {
t.Errorf("Analyzer.Analyze() got = %v, want nil", got)
}
return
}
// Bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.FullyQualifiedName == bindings[j].Resource.FullyQualifiedName {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.FullyQualifiedName < bindings[j].Resource.FullyQualifiedName
})
}
================================================
FILE: pkg/analyzer/analyzers/jira/models.go
================================================
package jira
import (
"sync"
)
const (
ResourceTypeProject = "Project"
ResourceTypeBoard = "Board"
ResourceTypeGroup = "Group"
ResourceTypeIssue = "Issue"
ResourceTypeUser = "User"
ResourceTypeAuditRecord = "AuditRecord"
)
var ResourcePermissions = map[string][]Permission{
ResourceTypeProject: {
Administer,
BrowseProjects,
AdministerProjects,
CreateProject,
EditIssueLayout,
ViewDevTools,
ViewAggregatedData,
SystemAdmin,
},
ResourceTypeIssue: {
Administer,
AddComments,
AssignIssues,
CloseIssues,
CreateAttachments,
CreateIssues,
DeleteIssues,
DeleteAllAttachments,
DeleteAllComments,
DeleteAllWorklogs,
DeleteOwnAttachments,
DeleteOwnComments,
DeleteOwnWorklogs,
EditAllComments,
EditAllWorklogs,
EditIssues,
EditOwnComments,
EditOwnWorklogs,
LinkIssues,
ManageWatchers,
ModifyReporter,
MoveIssues,
ResolveIssues,
ScheduleIssues,
SetIssueSecurity,
SystemAdmin,
TransitionIssues,
UnarchiveIssues,
ViewVotersAndWatchers,
WorkOnIssues,
},
ResourceTypeBoard: {
Administer,
ManageSprintsPermission,
BrowseProjects,
SystemAdmin,
ViewAggregatedData,
},
ResourceTypeUser: {
AssignableUser,
SystemAdmin,
UserPicker,
},
ResourceTypeGroup: {
Administer,
SystemAdmin,
},
ResourceTypeAuditRecord: {
Administer,
SystemAdmin,
},
}
type SecretInfo struct {
mu sync.RWMutex
UserInfo JiraUser
Permissions []string
Resources []JiraResource
}
// JiraUser represents the response from /myself API
type JiraUser struct {
AccountID string `json:"accountId"`
AccountType string `json:"accountType"`
DisplayName string `json:"displayName"`
EmailAddress string `json:"emailAddress"`
Active bool `json:"active"`
TimeZone string `json:"timeZone"`
Locale string `json:"locale"`
Self string `json:"self"`
}
type JiraResource struct {
ID string
Name string
Type string
Metadata map[string]string
Parent *JiraResource
Permissions []string
}
func (s *SecretInfo) appendResource(resource JiraResource, resourceType string) {
s.mu.Lock()
defer s.mu.Unlock()
if perms, ok := ResourcePermissions[resourceType]; ok {
for _, p := range perms {
if userPerms[p] {
resource.Permissions = append(resource.Permissions, PermissionStrings[p])
}
}
}
s.Resources = append(s.Resources, resource)
}
type JiraPermissionsResponse struct {
Permissions map[string]JiraPermission `json:"permissions"`
}
type JiraPermission struct {
ID string `json:"id"`
Key string `json:"key"`
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
HavePermission bool `json:"havePermission"`
}
type ProjectSearchResponse struct {
MaxResults int `json:"maxResults"`
Total int `json:"total"`
IsLast bool `json:"isLast"`
Values []JiraProject `json:"values"`
}
type JiraProject struct {
ID string `json:"id"`
Key string `json:"key"`
Name string `json:"name"`
ProjectTypeKey string `json:"projectTypeKey"`
IsPrivate bool `json:"isPrivate"`
UUID string `json:"uuid"`
}
type JiraIssue struct {
Issues []struct {
ID string `json:"id"`
Key string `json:"key"`
Fields struct {
Summary string `json:"summary"`
Status struct {
Name string `json:"name"`
} `json:"status"`
IssueType struct {
Name string `json:"name"`
} `json:"issuetype"`
} `json:"fields"`
} `json:"issues"`
}
type JiraBoard struct {
Values []struct {
ID int `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Self string `json:"self"`
IsPrivate bool `json:"isPrivate"`
Location struct {
ProjectID int `json:"projectId"`
DisplayName string `json:"displayName"`
ProjectName string `json:"projectName"`
ProjectKey string `json:"projectKey"`
ProjectTypeKey string `json:"projectTypeKey"`
AvatarURI string `json:"avatarURI"`
Name string `json:"name"`
} `json:"location"`
} `json:"values"`
}
type JiraGroup struct {
Total int `json:"total"`
Groups []struct {
Name string `json:"name"`
HTML string `json:"html"`
GroupID string `json:"groupId"`
Labels []struct {
Text string `json:"text"`
Title string `json:"title"`
Type string `json:"type"`
} `json:"labels"`
} `json:"groups"`
}
type AuditRecord struct {
Offset int `json:"offset"`
Limit int `json:"limit"`
Total int `json:"total"`
Records []struct {
ID int `json:"id"`
Summary string `json:"summary"`
Created string `json:"created"`
Category string `json:"category"`
EventSource string `json:"eventSource"`
RemoteAddress string `json:"remoteAddress,omitempty"`
AuthorKey string `json:"authorKey,omitempty"`
AuthorAccount string `json:"authorAccountId,omitempty"`
ObjectItem struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
TypeName string `json:"typeName"`
ParentID string `json:"parentId,omitempty"`
ParentName string `json:"parentName,omitempty"`
} `json:"objectItem"`
AssociatedItems []struct {
ID string `json:"id"`
Name string `json:"name"`
TypeName string `json:"typeName"`
ParentID string `json:"parentId"`
ParentName string `json:"parentName"`
} `json:"associatedItems"`
ChangedValues []struct {
FieldName string `json:"fieldName"`
ChangedTo string `json:"changedTo"`
} `json:"changedValues"`
} `json:"records"`
}
================================================
FILE: pkg/analyzer/analyzers/jira/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package jira
import "errors"
type Permission int
const (
Invalid Permission = iota
AddComments Permission = iota
Administer Permission = iota
AdministerProjects Permission = iota
AssignableUser Permission = iota
AssignIssues Permission = iota
BrowseProjects Permission = iota
BulkChange Permission = iota
CloseIssues Permission = iota
CreateAttachments Permission = iota
CreateIssues Permission = iota
CreateProject Permission = iota
CreateSharedObjects Permission = iota
DeleteAllAttachments Permission = iota
DeleteAllComments Permission = iota
DeleteAllWorklogs Permission = iota
DeleteIssues Permission = iota
DeleteOwnAttachments Permission = iota
DeleteOwnComments Permission = iota
DeleteOwnWorklogs Permission = iota
EditAllComments Permission = iota
EditAllWorklogs Permission = iota
EditIssues Permission = iota
EditIssueLayout Permission = iota
EditOwnComments Permission = iota
EditOwnWorklogs Permission = iota
EditWorkflow Permission = iota
LinkIssues Permission = iota
ManageGroupFilterSubscriptions Permission = iota
ManageSprintsPermission Permission = iota
ManageWatchers Permission = iota
ModifyReporter Permission = iota
MoveIssues Permission = iota
ResolveIssues Permission = iota
ScheduleIssues Permission = iota
SetIssueSecurity Permission = iota
SystemAdmin Permission = iota
TransitionIssues Permission = iota
UnarchiveIssues Permission = iota
UserPicker Permission = iota
ViewAggregatedData Permission = iota
ViewDevTools Permission = iota
ViewReadonlyWorkflow Permission = iota
ViewVotersAndWatchers Permission = iota
WorkOnIssues Permission = iota
)
var (
PermissionStrings = map[Permission]string{
AddComments: "add_comments",
Administer: "administer",
AdministerProjects: "administer_projects",
AssignableUser: "assignable_user",
AssignIssues: "assign_issues",
BrowseProjects: "browse_projects",
BulkChange: "bulk_change",
CloseIssues: "close_issues",
CreateAttachments: "create_attachments",
CreateIssues: "create_issues",
CreateProject: "create_project",
CreateSharedObjects: "create_shared_objects",
DeleteAllAttachments: "delete_all_attachments",
DeleteAllComments: "delete_all_comments",
DeleteAllWorklogs: "delete_all_worklogs",
DeleteIssues: "delete_issues",
DeleteOwnAttachments: "delete_own_attachments",
DeleteOwnComments: "delete_own_comments",
DeleteOwnWorklogs: "delete_own_worklogs",
EditAllComments: "edit_all_comments",
EditAllWorklogs: "edit_all_worklogs",
EditIssues: "edit_issues",
EditIssueLayout: "edit_issue_layout",
EditOwnComments: "edit_own_comments",
EditOwnWorklogs: "edit_own_worklogs",
EditWorkflow: "edit_workflow",
LinkIssues: "link_issues",
ManageGroupFilterSubscriptions: "manage_group_filter_subscriptions",
ManageSprintsPermission: "manage_sprints_permission",
ManageWatchers: "manage_watchers",
ModifyReporter: "modify_reporter",
MoveIssues: "move_issues",
ResolveIssues: "resolve_issues",
ScheduleIssues: "schedule_issues",
SetIssueSecurity: "set_issue_security",
SystemAdmin: "system_admin",
TransitionIssues: "transition_issues",
UnarchiveIssues: "unarchive_issues",
UserPicker: "user_picker",
ViewAggregatedData: "view_aggregated_data",
ViewDevTools: "view_dev_tools",
ViewReadonlyWorkflow: "view_readonly_workflow",
ViewVotersAndWatchers: "view_voters_and_watchers",
WorkOnIssues: "work_on_issues",
}
StringToPermission = map[string]Permission{
"add_comments": AddComments,
"administer": Administer,
"administer_projects": AdministerProjects,
"assignable_user": AssignableUser,
"assign_issues": AssignIssues,
"browse_projects": BrowseProjects,
"bulk_change": BulkChange,
"close_issues": CloseIssues,
"create_attachments": CreateAttachments,
"create_issues": CreateIssues,
"create_project": CreateProject,
"create_shared_objects": CreateSharedObjects,
"delete_all_attachments": DeleteAllAttachments,
"delete_all_comments": DeleteAllComments,
"delete_all_worklogs": DeleteAllWorklogs,
"delete_issues": DeleteIssues,
"delete_own_attachments": DeleteOwnAttachments,
"delete_own_comments": DeleteOwnComments,
"delete_own_worklogs": DeleteOwnWorklogs,
"edit_all_comments": EditAllComments,
"edit_all_worklogs": EditAllWorklogs,
"edit_issues": EditIssues,
"edit_issue_layout": EditIssueLayout,
"edit_own_comments": EditOwnComments,
"edit_own_worklogs": EditOwnWorklogs,
"edit_workflow": EditWorkflow,
"link_issues": LinkIssues,
"manage_group_filter_subscriptions": ManageGroupFilterSubscriptions,
"manage_sprints_permission": ManageSprintsPermission,
"manage_watchers": ManageWatchers,
"modify_reporter": ModifyReporter,
"move_issues": MoveIssues,
"resolve_issues": ResolveIssues,
"schedule_issues": ScheduleIssues,
"set_issue_security": SetIssueSecurity,
"system_admin": SystemAdmin,
"transition_issues": TransitionIssues,
"unarchive_issues": UnarchiveIssues,
"user_picker": UserPicker,
"view_aggregated_data": ViewAggregatedData,
"view_dev_tools": ViewDevTools,
"view_readonly_workflow": ViewReadonlyWorkflow,
"view_voters_and_watchers": ViewVotersAndWatchers,
"work_on_issues": WorkOnIssues,
}
PermissionIDs = map[Permission]int{
AddComments: 1,
Administer: 2,
AdministerProjects: 3,
AssignableUser: 4,
AssignIssues: 5,
BrowseProjects: 6,
BulkChange: 7,
CloseIssues: 8,
CreateAttachments: 9,
CreateIssues: 10,
CreateProject: 11,
CreateSharedObjects: 12,
DeleteAllAttachments: 13,
DeleteAllComments: 14,
DeleteAllWorklogs: 15,
DeleteIssues: 16,
DeleteOwnAttachments: 17,
DeleteOwnComments: 18,
DeleteOwnWorklogs: 19,
EditAllComments: 20,
EditAllWorklogs: 21,
EditIssues: 22,
EditIssueLayout: 23,
EditOwnComments: 24,
EditOwnWorklogs: 25,
EditWorkflow: 26,
LinkIssues: 27,
ManageGroupFilterSubscriptions: 28,
ManageSprintsPermission: 29,
ManageWatchers: 30,
ModifyReporter: 31,
MoveIssues: 32,
ResolveIssues: 33,
ScheduleIssues: 34,
SetIssueSecurity: 35,
SystemAdmin: 36,
TransitionIssues: 37,
UnarchiveIssues: 38,
UserPicker: 39,
ViewAggregatedData: 40,
ViewDevTools: 41,
ViewReadonlyWorkflow: 42,
ViewVotersAndWatchers: 43,
WorkOnIssues: 44,
}
IdToPermission = map[int]Permission{
1: AddComments,
2: Administer,
3: AdministerProjects,
4: AssignableUser,
5: AssignIssues,
6: BrowseProjects,
7: BulkChange,
8: CloseIssues,
9: CreateAttachments,
10: CreateIssues,
11: CreateProject,
12: CreateSharedObjects,
13: DeleteAllAttachments,
14: DeleteAllComments,
15: DeleteAllWorklogs,
16: DeleteIssues,
17: DeleteOwnAttachments,
18: DeleteOwnComments,
19: DeleteOwnWorklogs,
20: EditAllComments,
21: EditAllWorklogs,
22: EditIssues,
23: EditIssueLayout,
24: EditOwnComments,
25: EditOwnWorklogs,
26: EditWorkflow,
27: LinkIssues,
28: ManageGroupFilterSubscriptions,
29: ManageSprintsPermission,
30: ManageWatchers,
31: ModifyReporter,
32: MoveIssues,
33: ResolveIssues,
34: ScheduleIssues,
35: SetIssueSecurity,
36: SystemAdmin,
37: TransitionIssues,
38: UnarchiveIssues,
39: UserPicker,
40: ViewAggregatedData,
41: ViewDevTools,
42: ViewReadonlyWorkflow,
43: ViewVotersAndWatchers,
44: WorkOnIssues,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/jira/permissions.yaml
================================================
permissions:
- add_comments
- administer
- administer_projects
- assignable_user
- assign_issues
- browse_projects
- bulk_change
- close_issues
- create_attachments
- create_issues
- create_project
- create_shared_objects
- delete_all_attachments
- delete_all_comments
- delete_all_worklogs
- delete_issues
- delete_own_attachments
- delete_own_comments
- delete_own_worklogs
- edit_all_comments
- edit_all_worklogs
- edit_issues
- edit_issue_layout
- edit_own_comments
- edit_own_worklogs
- edit_workflow
- link_issues
- manage_group_filter_subscriptions
- manage_sprints_permission
- manage_watchers
- modify_reporter
- move_issues
- resolve_issues
- schedule_issues
- set_issue_security
- system_admin
- transition_issues
- unarchive_issues
- user_picker
- view_aggregated_data
- view_dev_tools
- view_readonly_workflow
- view_voters_and_watchers
- work_on_issues
================================================
FILE: pkg/analyzer/analyzers/jira/requests.go
================================================
package jira
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
)
type endpoint int
const (
// list of endpoints
mySelf endpoint = iota
myPermissions
getAllProjects
searchIssues
getAllBoards
getAllUsers
findGroups
getAuditRecords
)
var (
baseURL = "https://%s/rest"
// endpoints contain Jira API endpoints
endpoints = map[endpoint]string{
mySelf: "myself",
myPermissions: "mypermissions",
searchIssues: "search/jql",
getAllProjects: "project/search",
getAllBoards: "board",
getAllUsers: "users/search",
findGroups: "groups/picker",
getAuditRecords: "auditing/record",
}
userPerms = make(map[Permission]bool)
)
// buildBasicAuthHeader constructs the Basic Auth header
func buildBasicAuthHeader(email, token string) string {
auth := fmt.Sprintf("%s:%s", email, token)
return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
}
// makeJiraRequest send the API request to passed url with passed key as API Key and return response body and status code
func makeJiraRequest(client *http.Client, endpoint, email, token string) ([]byte, int, error) {
// create request
req, err := http.NewRequest(http.MethodGet, endpoint, http.NoBody)
if err != nil {
return nil, 0, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", buildBasicAuthHeader(email, token))
resp, err := client.Do(req)
if err != nil {
return nil, 0, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
responseBodyByte, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, err
}
return responseBodyByte, resp.StatusCode, nil
}
func capturePermissions(client *http.Client, domain, email, token string) ([]byte, int, error) {
var allPermissions []string
for _, key := range PermissionStrings {
allPermissions = append(allPermissions, strings.ToUpper(key))
}
query := url.Values{}
query.Set("permissions", strings.Join(allPermissions, ","))
endpoint := fmt.Sprintf("%s/api/3/%s?%s", fmt.Sprintf(baseURL, domain), endpoints[myPermissions], query.Encode())
return makeJiraRequest(client, endpoint, email, token)
}
// captureResources try to capture all the resource that the key can access
func captureResources(client *http.Client, domain, email, token string, secretInfo *SecretInfo, grantedPermissions []string) error {
for _, p := range grantedPermissions {
userPerms[StringToPermission[strings.ToLower(p)]] = true
}
var (
wg sync.WaitGroup
errAggWg sync.WaitGroup
aggregatedErrs = make([]error, 0)
errChan = make(chan error, 1)
)
errAggWg.Add(1)
go func() {
defer errAggWg.Done()
for err := range errChan {
aggregatedErrs = append(aggregatedErrs, err)
}
}()
launchTask := func(task func() error) {
wg.Add(1)
go func() {
defer wg.Done()
if err := task(); err != nil {
errChan <- err
}
}()
}
projects, err := captureProjects(client, domain, email, token, secretInfo)
if err != nil {
return fmt.Errorf("failed to capture projects: %w", err)
}
if projects != nil {
for _, proj := range projects.Values {
launchTask(func() error {
return captureIssues(client, domain, email, token, proj.Key, secretInfo)
})
}
}
launchTask(func() error { return captureBoards(client, domain, email, token, secretInfo) })
launchTask(func() error { return captureUsers(client, domain, email, token, secretInfo) })
launchTask(func() error { return captureGroups(client, domain, email, token, secretInfo) })
launchTask(func() error { return captureAuditLogs(client, domain, email, token, secretInfo) })
wg.Wait()
close(errChan)
errAggWg.Wait()
if len(aggregatedErrs) > 0 {
return errors.Join(aggregatedErrs...)
}
return nil
}
// captureUserInfo calls `/myself` API and store the current user information in secretInfo
func captureUserInfo(client *http.Client, token, domain, email string, secretInfo *SecretInfo) error {
endPoint := fmt.Sprintf("%s/api/3/%s", fmt.Sprintf(baseURL, domain), endpoints[mySelf])
respBody, statusCode, err := makeJiraRequest(client, endPoint, email, token)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var user JiraUser
if err := json.Unmarshal(respBody, &user); err != nil {
return err
}
secretInfo.UserInfo = user
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return fmt.Errorf("invalid email or api token")
case http.StatusNotFound:
return fmt.Errorf("domain not found: %s", domain)
default:
return fmt.Errorf("unexpected status code: %d for API: %s", statusCode, endpoints[mySelf])
}
}
func captureProjects(client *http.Client, domain, email, token string, secretInfo *SecretInfo) (*ProjectSearchResponse, error) {
endpoint := fmt.Sprintf("%s/api/3/%s", fmt.Sprintf(baseURL, domain), endpoints[getAllProjects])
body, statusCode, err := makeJiraRequest(client, endpoint, email, token)
if err != nil {
return nil, err
}
if err := handleStatusCode(statusCode, endpoint); err != nil {
return nil, err
}
var resp ProjectSearchResponse
if err := json.Unmarshal(body, &resp); err != nil {
return nil, fmt.Errorf("failed to unmarshal project response: %w", err)
}
for _, proj := range resp.Values {
resource := JiraResource{
ID: proj.ID,
Name: proj.Name,
Type: ResourceTypeProject,
Metadata: map[string]string{
"Key": proj.Key,
"UUID": proj.UUID,
"Private": strconv.FormatBool(proj.IsPrivate),
"TypeKey": proj.ProjectTypeKey,
},
}
secretInfo.appendResource(resource, ResourceTypeProject)
}
return &resp, nil
}
func captureIssues(client *http.Client, domain, email, token, projectKey string, secretInfo *SecretInfo) error {
path := fmt.Sprintf("api/3/%s", endpoints[searchIssues])
query := fmt.Sprintf("jql=project=%s&fields=issuetype,summary,status", projectKey)
endpoint := fmt.Sprintf("%s/%s?%s", fmt.Sprintf(baseURL, domain), path, query)
body, statusCode, err := makeJiraRequest(client, endpoint, email, token)
if err != nil {
return err
}
if err := handleStatusCode(statusCode, endpoint); err != nil {
return err
}
var issueResp JiraIssue
if err := json.Unmarshal(body, &issueResp); err != nil {
return fmt.Errorf("failed to unmarshal issue response: %w", err)
}
for _, issue := range issueResp.Issues {
issueResource := JiraResource{
ID: issue.ID,
Name: issue.Key,
Type: issue.Fields.IssueType.Name,
Metadata: map[string]string{
"Summary": issue.Fields.Summary,
"Status": issue.Fields.Status.Name,
"Project": projectKey,
},
}
secretInfo.appendResource(issueResource, ResourceTypeIssue)
}
return nil
}
func captureBoards(client *http.Client, domain, email, token string, secretInfo *SecretInfo) error {
endpoint := fmt.Sprintf("%s/agile/1.0/%s", fmt.Sprintf(baseURL, domain), endpoints[getAllBoards])
body, statusCode, err := makeJiraRequest(client, endpoint, email, token)
if err != nil {
return err
}
if err := handleStatusCode(statusCode, endpoint); err != nil {
return err
}
var boardResp JiraBoard
if err := json.Unmarshal(body, &boardResp); err != nil {
return fmt.Errorf("failed to unmarshal board response: %w", err)
}
for _, board := range boardResp.Values {
boardResource := JiraResource{
ID: fmt.Sprintf("%d", board.ID),
Name: board.Name,
Type: ResourceTypeBoard,
Metadata: map[string]string{
"BoardType": board.Type,
"IsPrivate": strconv.FormatBool(board.IsPrivate),
"ProjectID": fmt.Sprintf("%d", board.Location.ProjectID),
"ProjectKey": board.Location.ProjectKey,
"ProjectName": board.Location.ProjectName,
"ProjectType": board.Location.ProjectTypeKey,
"DisplayName": board.Location.DisplayName,
"AvatarURI": board.Location.AvatarURI,
"BoardSelfURL": board.Self,
},
}
secretInfo.appendResource(boardResource, ResourceTypeBoard)
}
return nil
}
func captureUsers(client *http.Client, domain, email, token string, secretInfo *SecretInfo) error {
endpoint := fmt.Sprintf("%s/api/3/%s", fmt.Sprintf(baseURL, domain), endpoints[getAllUsers])
body, statusCode, err := makeJiraRequest(client, endpoint, email, token)
if err != nil {
return err
}
if err := handleStatusCode(statusCode, endpoint); err != nil {
return err
}
var users []JiraUser
if err := json.Unmarshal(body, &users); err != nil {
return fmt.Errorf("failed to unmarshal user response: %w", err)
}
for _, user := range users {
userResource := JiraResource{
ID: user.AccountID,
Name: user.DisplayName,
Type: ResourceTypeUser,
Metadata: map[string]string{
"Email": user.EmailAddress,
"AccountType": user.AccountType,
"Active": strconv.FormatBool(user.Active),
"SelfURL": user.Self,
},
}
if user.AccountType != "app" {
secretInfo.appendResource(userResource, ResourceTypeUser)
}
}
return nil
}
func captureGroups(client *http.Client, domain, email, token string, secretInfo *SecretInfo) error {
endpoint := fmt.Sprintf("%s/api/3/%s", fmt.Sprintf(baseURL, domain), endpoints[findGroups])
body, statusCode, err := makeJiraRequest(client, endpoint, email, token)
if err != nil {
return err
}
if err := handleStatusCode(statusCode, endpoint); err != nil {
return err
}
var groupResp JiraGroup
if err := json.Unmarshal(body, &groupResp); err != nil {
return fmt.Errorf("failed to unmarshal group response: %w", err)
}
for _, group := range groupResp.Groups {
metadata := map[string]string{
"HTML": group.HTML,
}
if len(group.Labels) > 0 {
for i, label := range group.Labels {
metadata[fmt.Sprintf("Label%d_Text", i)] = label.Text
metadata[fmt.Sprintf("Label%d_Title", i)] = label.Title
metadata[fmt.Sprintf("Label%d_Type", i)] = label.Type
}
}
groupResource := JiraResource{
ID: group.GroupID,
Name: group.Name,
Type: ResourceTypeGroup,
Metadata: metadata,
}
secretInfo.appendResource(groupResource, ResourceTypeGroup)
}
return nil
}
func captureAuditLogs(client *http.Client, domain, email, token string, secretInfo *SecretInfo) error {
endpoint := fmt.Sprintf("%s/api/3/%s", fmt.Sprintf(baseURL, domain), endpoints[getAuditRecords])
body, statusCode, err := makeJiraRequest(client, endpoint, email, token)
if err != nil {
return err
}
if err := handleStatusCode(statusCode, endpoint); err != nil {
return err
}
var auditResp AuditRecord
if err := json.Unmarshal(body, &auditResp); err != nil {
return fmt.Errorf("failed to unmarshal audit logs: %w", err)
}
for _, record := range auditResp.Records {
metadata := map[string]string{
"Summary": record.Summary,
"Created": record.Created,
"Category": record.Category,
"Type": record.ObjectItem.TypeName,
"Object": record.ObjectItem.Name,
}
if record.AuthorAccount != "" {
metadata["AuthorAccountID"] = record.AuthorAccount
}
if record.RemoteAddress != "" {
metadata["RemoteAddress"] = record.RemoteAddress
}
for i, item := range record.AssociatedItems {
metadata[fmt.Sprintf("AssociatedItem%d_Name", i)] = item.Name
metadata[fmt.Sprintf("AssociatedItem%d_Type", i)] = item.TypeName
}
for i, change := range record.ChangedValues {
metadata[fmt.Sprintf("ChangedField%d_Name", i)] = change.FieldName
metadata[fmt.Sprintf("ChangedField%d_To", i)] = change.ChangedTo
}
resource := JiraResource{
ID: fmt.Sprintf("%d", record.ID),
Name: record.Summary,
Type: ResourceTypeAuditRecord,
Metadata: metadata,
}
secretInfo.appendResource(resource, ResourceTypeAuditRecord)
}
return nil
}
func handleStatusCode(statusCode int, endpoint string) error {
switch {
case statusCode == http.StatusOK:
return nil
case statusCode == http.StatusBadRequest:
return fmt.Errorf("bad request for API: %s", endpoint)
case statusCode == http.StatusUnauthorized, statusCode == http.StatusForbidden,
statusCode == http.StatusNotFound, statusCode == http.StatusConflict:
return nil
default:
return fmt.Errorf("unexpected status code: %d for API: %s", statusCode, endpoint)
}
}
================================================
FILE: pkg/analyzer/analyzers/jira/result_output.json
================================================
{
"AnalyzerType": 42,
"Bindings": [
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10000",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "development",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Includes development summary panel information used in JQL",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Dev Summary Custom Field",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "development",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10001",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Team",
"ChangedField1_Name": "Description",
"ChangedField1_To": "",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Team",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Team",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10002",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Organizations",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Stores the organizations that are associated with a Service Desk customer portal requests. This custom field is created programmatically and required by Service Desk.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Organizations",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Organizations",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10003",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Approvers",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Contains users needed for approval. This custom field was created by Jira Service Desk.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "User Picker (multiple users)",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Approvers",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10004",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Impact",
"ChangedField1_Name": "Description",
"ChangedField1_To": "",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Select List (single choice)",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Impact",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10005",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Change type",
"ChangedField1_Name": "Description",
"ChangedField1_To": "",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Select List (single choice)",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Change type",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10006",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Change risk",
"ChangedField1_Name": "Description",
"ChangedField1_To": "",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Select List (single choice)",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Change risk",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10007",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Change reason",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Choose the reason for the change request",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Select List (single choice)",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Change reason",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10008",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Actual start",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Enter when the change actually started.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Date Time Picker",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Actual start",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10009",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Actual end",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Enter when the change actually ended.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Date Time Picker",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Actual end",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10010",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Request Type",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Holds information about which Service Desk was used to create a ticket. This custom field is created programmatically and must not be modified.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Customer Request Type Custom Field",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Request Type",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10011",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Epic Name",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Provide a short name to identify this epic.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Name of Epic",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Epic Name",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10012",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Epic Status",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Epic Status field for Jira Software use only.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Status of Epic",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Epic Status",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10013",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Epic Color",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Epic Color field for Jira Software use only.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Color of Epic",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Epic Color",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10014",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Epic Link",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Choose an epic to assign this issue to.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Epic Link Relationship",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Epic Link",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10015",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Start date",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Allows the planned start date for a piece of work to be set.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Date Picker",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Start date",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10016",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Story point estimate",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Measurement of complexity and/or size of a requirement.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Number Field",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Story point estimate",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10017",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Issue color",
"ChangedField1_Name": "Description",
"ChangedField1_To": "",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Issue color",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Issue color",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10018",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Parent Link",
"ChangedField1_Name": "Description",
"ChangedField1_To": "",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Parent Link",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Parent Link",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field updated",
"FullyQualifiedName": "AuditRecord/10019",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Parent Link",
"Summary": "Custom field updated",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field updated",
"FullyQualifiedName": "AuditRecord/10020",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Story point estimate",
"Summary": "Custom field updated",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10021",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Rank",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Global rank field for Jira Software use only.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Global Rank",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Rank",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10022",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Sprint",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Jira Software sprint field",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Jira Sprint Field",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Sprint",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10023",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Flagged",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Allows to flag issues with impediments.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Checkboxes",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Flagged",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field context created",
"FullyQualifiedName": "AuditRecord/10024",
"Type": "AuditRecord",
"Metadata": {
"Category": "custom field context",
"ChangedField0_Name": "Associated to project(s)",
"ChangedField0_To": "All projects",
"ChangedField1_Name": "Associated to issue type(s)",
"ChangedField1_To": "All issue types",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "customfield_10022",
"Summary": "Field context created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10025",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Target start",
"ChangedField1_Name": "Description",
"ChangedField1_To": "The targeted start date. This custom field is created and required by Advanced Roadmaps for Jira.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Target start",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Target start",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field context created",
"FullyQualifiedName": "AuditRecord/10026",
"Type": "AuditRecord",
"Metadata": {
"Category": "custom field context",
"ChangedField0_Name": "Associated to project(s)",
"ChangedField0_To": "All projects",
"ChangedField1_Name": "Associated to issue type(s)",
"ChangedField1_To": "All issue types",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "customfield_10023",
"Summary": "Field context created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10027",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Target end",
"ChangedField1_Name": "Description",
"ChangedField1_To": "The targeted end date. This custom field is created and required by Advanced Roadmaps for Jira.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Target end",
"Created": "2025-05-05T10:25:48.747+0000",
"Object": "Target end",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Global permission added",
"FullyQualifiedName": "AuditRecord/10028",
"Type": "AuditRecord",
"Metadata": {
"Category": "permissions",
"ChangedField0_Name": "Permission",
"ChangedField0_To": "Administer Jira",
"ChangedField1_Name": "Group name",
"ChangedField1_To": "org-admins",
"ChangedField2_Name": "Group",
"ChangedField2_To": "f94760f8-4a5e-49da-ac1e-0d4281e86aa1",
"Created": "2025-05-20T09:11:40.435+0000",
"Object": "Global Permissions",
"Summary": "Global permission added",
"Type": "PERMISSIONS"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Global permission added",
"FullyQualifiedName": "AuditRecord/10029",
"Type": "AuditRecord",
"Metadata": {
"Category": "permissions",
"ChangedField0_Name": "Permission",
"ChangedField0_To": "Administer Jira",
"ChangedField1_Name": "Group name",
"ChangedField1_To": "jira-admins-shaider",
"ChangedField2_Name": "Group",
"ChangedField2_To": "e06e77c1-dea1-42ba-a119-ed1189826165",
"Created": "2025-05-20T09:11:40.465+0000",
"Object": "Global Permissions",
"Summary": "Global permission added",
"Type": "PERMISSIONS"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field context created",
"FullyQualifiedName": "AuditRecord/10030",
"Type": "AuditRecord",
"Metadata": {
"Category": "custom field context",
"ChangedField0_Name": "Associated to project(s)",
"ChangedField0_To": "All projects",
"ChangedField1_Name": "Associated to issue type(s)",
"ChangedField1_To": "All issue types",
"Created": "2025-05-20T09:11:41.700+0000",
"Object": "customfield_10026",
"Summary": "Field context created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10031",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "[CHART] Date of First Response",
"ChangedField1_Name": "Description",
"ChangedField1_To": "",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Date of First Response",
"Created": "2025-05-20T09:11:41.715+0000",
"Object": "[CHART] Date of First Response",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field context created",
"FullyQualifiedName": "AuditRecord/10032",
"Type": "AuditRecord",
"Metadata": {
"Category": "custom field context",
"ChangedField0_Name": "Associated to project(s)",
"ChangedField0_To": "All projects",
"ChangedField1_Name": "Associated to issue type(s)",
"ChangedField1_To": "All issue types",
"Created": "2025-05-20T09:11:41.812+0000",
"Object": "customfield_10027",
"Summary": "Field context created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10033",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "[CHART] Time in Status",
"ChangedField1_Name": "Description",
"ChangedField1_To": "",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Time in Status",
"Created": "2025-05-20T09:11:41.830+0000",
"Object": "[CHART] Time in Status",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field context created",
"FullyQualifiedName": "AuditRecord/10034",
"Type": "AuditRecord",
"Metadata": {
"Category": "custom field context",
"ChangedField0_Name": "Associated to project(s)",
"ChangedField0_To": "All projects",
"ChangedField1_Name": "Associated to issue type(s)",
"ChangedField1_To": "All issue types",
"Created": "2025-05-20T09:11:43.498+0000",
"Object": "customfield_10029",
"Summary": "Field context created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field context created",
"FullyQualifiedName": "AuditRecord/10035",
"Type": "AuditRecord",
"Metadata": {
"Category": "custom field context",
"ChangedField0_Name": "Associated to project(s)",
"ChangedField0_To": "All projects",
"ChangedField1_Name": "Associated to issue type(s)",
"ChangedField1_To": "All issue types",
"Created": "2025-05-20T09:11:43.499+0000",
"Object": "customfield_10031",
"Summary": "Field context created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field context created",
"FullyQualifiedName": "AuditRecord/10036",
"Type": "AuditRecord",
"Metadata": {
"Category": "custom field context",
"ChangedField0_Name": "Associated to project(s)",
"ChangedField0_To": "All projects",
"ChangedField1_Name": "Associated to issue type(s)",
"ChangedField1_To": "All issue types",
"Created": "2025-05-20T09:11:43.499+0000",
"Object": "customfield_10028",
"Summary": "Field context created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field context created",
"FullyQualifiedName": "AuditRecord/10037",
"Type": "AuditRecord",
"Metadata": {
"Category": "custom field context",
"ChangedField0_Name": "Associated to project(s)",
"ChangedField0_To": "All projects",
"ChangedField1_Name": "Associated to issue type(s)",
"ChangedField1_To": "All issue types",
"Created": "2025-05-20T09:11:43.499+0000",
"Object": "customfield_10030",
"Summary": "Field context created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10038",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Open forms",
"ChangedField1_Name": "Description",
"ChangedField1_To": "The number of open forms on the issue",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Open forms",
"Created": "2025-05-20T09:11:43.520+0000",
"Object": "Open forms",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10039",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Submitted forms",
"ChangedField1_Name": "Description",
"ChangedField1_To": "The number of submitted forms on the issue",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Submitted forms",
"Created": "2025-05-20T09:11:43.520+0000",
"Object": "Submitted forms",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10040",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Total forms",
"ChangedField1_Name": "Description",
"ChangedField1_To": "The total number of forms on the issue",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Total forms",
"Created": "2025-05-20T09:11:43.523+0000",
"Object": "Total forms",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10041",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Locked forms",
"ChangedField1_Name": "Description",
"ChangedField1_To": "The number of locked forms on the issue",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Locked forms",
"Created": "2025-05-20T09:11:43.536+0000",
"Object": "Locked forms",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Issue type created",
"FullyQualifiedName": "AuditRecord/10042",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "issue types",
"Created": "2025-05-20T09:12:06.211+0000",
"Object": "Epic",
"Summary": "Issue type created",
"Type": "ISSUE_TYPE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Issue type created",
"FullyQualifiedName": "AuditRecord/10043",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "issue types",
"Created": "2025-05-20T09:12:06.332+0000",
"Object": "Subtask",
"Summary": "Issue type created",
"Type": "ISSUE_TYPE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Issue type created",
"FullyQualifiedName": "AuditRecord/10044",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "issue types",
"Created": "2025-05-20T09:12:06.400+0000",
"Object": "Task",
"Summary": "Issue type created",
"Type": "ISSUE_TYPE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Issue type created",
"FullyQualifiedName": "AuditRecord/10045",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "issue types",
"Created": "2025-05-20T09:12:06.468+0000",
"Object": "Story",
"Summary": "Issue type created",
"Type": "ISSUE_TYPE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10046",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "development",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Includes development summary panel information used in JQL",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Dev Summary Custom Field",
"Created": "2025-05-20T09:12:09.092+0000",
"Object": "development",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10047",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Design",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Custom field that stores design information for JQL",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Design",
"Created": "2025-05-20T09:12:09.180+0000",
"Object": "Design",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10048",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Vulnerability",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Custom field that stores vulnerability information for JQL",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Vulnerability",
"Created": "2025-05-20T09:12:09.257+0000",
"Object": "Vulnerability",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Group created",
"FullyQualifiedName": "AuditRecord/10049",
"Type": "AuditRecord",
"Metadata": {
"Category": "group management",
"Created": "2025-05-20T09:12:11.379+0000",
"Object": "jira-users-shaider",
"Summary": "Group created",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Group created",
"FullyQualifiedName": "AuditRecord/10050",
"Type": "AuditRecord",
"Metadata": {
"Category": "group management",
"Created": "2025-05-20T09:12:11.397+0000",
"Object": "org-admins",
"Summary": "Group created",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Group created",
"FullyQualifiedName": "AuditRecord/10051",
"Type": "AuditRecord",
"Metadata": {
"Category": "group management",
"Created": "2025-05-20T09:12:11.399+0000",
"Object": "atlassian-addons-admin",
"Summary": "Group created",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Group created",
"FullyQualifiedName": "AuditRecord/10052",
"Type": "AuditRecord",
"Metadata": {
"Category": "group management",
"Created": "2025-05-20T09:12:11.408+0000",
"Object": "jira-admins-shaider",
"Summary": "Group created",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Group created",
"FullyQualifiedName": "AuditRecord/10053",
"Type": "AuditRecord",
"Metadata": {
"Category": "group management",
"Created": "2025-05-20T09:12:11.408+0000",
"Object": "jira-user-access-admins-shaider",
"Summary": "Group created",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Group created",
"FullyQualifiedName": "AuditRecord/10054",
"Type": "AuditRecord",
"Metadata": {
"Category": "group management",
"Created": "2025-05-20T09:12:11.414+0000",
"Object": "system-administrators",
"Summary": "Group created",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10055",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "d1e68c57-d711-4b14-a2c2-ef4bd13e60b9",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:12:11.414+0000",
"Object": "jira-users-shaider",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10056",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "d1e68c57-d711-4b14-a2c2-ef4bd13e60b9",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:12:11.407+0000",
"Object": "org-admins",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10057",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "d1e68c57-d711-4b14-a2c2-ef4bd13e60b9",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T09:12:11.405+0000",
"Object": "d1e68c57-d711-4b14-a2c2-ef4bd13e60b9",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Workflow created",
"FullyQualifiedName": "AuditRecord/10058",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "workflows",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Software workflow for project SCRUM",
"ChangedField1_Name": "Description",
"ChangedField1_To": "",
"Created": "2025-05-20T09:12:11.729+0000",
"Object": "Software workflow for project SCRUM",
"Summary": "Workflow created",
"Type": "WORKFLOW"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10059",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "My Scrum Project",
"AssociatedItem0_Type": "PROJECT",
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Created": "2025-05-20T09:12:14.634+0000",
"Object": "Administrator",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Sprint created",
"FullyQualifiedName": "AuditRecord/10060",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "sprints",
"Created": "2025-05-20T09:12:19.817+0000",
"Object": "SCRUM Sprint 1",
"Summary": "Sprint created",
"Type": "SPRINT"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field removed from Screen",
"FullyQualifiedName": "AuditRecord/10061",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "screens",
"ChangedField0_Name": "Field Removed",
"ChangedField0_To": "",
"Created": "2025-05-20T09:12:20.260+0000",
"Object": "SCRUM-Story",
"Summary": "Field removed from Screen",
"Type": "SCREEN"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field updated in Screen",
"FullyQualifiedName": "AuditRecord/10062",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "screens",
"ChangedField0_Name": "Field Updated",
"ChangedField0_To": "Resolution,Reporter,Summary,Flagged,Labels,Attachment,Restrict to,Rank,Assignee,Story point estimate,Linked Issues,Issue Type,Team,Sprint,Description,development,Parent",
"Created": "2025-05-20T09:12:20.282+0000",
"Object": "SCRUM-Story",
"Summary": "Field updated in Screen",
"Type": "SCREEN"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field removed from Screen",
"FullyQualifiedName": "AuditRecord/10063",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "screens",
"ChangedField0_Name": "Field Removed",
"ChangedField0_To": "",
"Created": "2025-05-20T09:12:20.363+0000",
"Object": "SCRUM-Task",
"Summary": "Field removed from Screen",
"Type": "SCREEN"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field updated in Screen",
"FullyQualifiedName": "AuditRecord/10064",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "screens",
"ChangedField0_Name": "Field Updated",
"ChangedField0_To": "Flagged,Issue Type,Labels,Attachment,Team,Rank,Story point estimate,development,Parent,Reporter,Restrict to,Description,Assignee,Summary,Linked Issues,Resolution,Sprint",
"Created": "2025-05-20T09:12:20.383+0000",
"Object": "SCRUM-Task",
"Summary": "Field updated in Screen",
"Type": "SCREEN"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field removed from Screen",
"FullyQualifiedName": "AuditRecord/10065",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "screens",
"ChangedField0_Name": "Field Removed",
"ChangedField0_To": "",
"Created": "2025-05-20T09:12:20.464+0000",
"Object": "SCRUM - Subtask",
"Summary": "Field removed from Screen",
"Type": "SCREEN"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field updated in Screen",
"FullyQualifiedName": "AuditRecord/10066",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "screens",
"ChangedField0_Name": "Field Updated",
"ChangedField0_To": "Labels,Attachment,Restrict to,Assignee,Linked Issues,Issue Type,Sprint,development,Rank,Reporter,Summary,Flagged,Story point estimate,Description,Team,Parent,Resolution",
"Created": "2025-05-20T09:12:20.478+0000",
"Object": "SCRUM - Subtask",
"Summary": "Field updated in Screen",
"Type": "SCREEN"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field configuration scheme updated",
"FullyQualifiedName": "AuditRecord/10067",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "fields",
"ChangedField0_Name": "Issue Type",
"ChangedField0_To": "Task",
"ChangedField1_Name": "Field Configuration",
"ChangedField1_To": "LEARNJIRA-10005",
"Created": "2025-05-20T09:12:28.876+0000",
"Object": "Field Configuration Scheme for Project LEARNJIRA",
"Summary": "Field configuration scheme updated",
"Type": "SCHEME"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Issue type created",
"FullyQualifiedName": "AuditRecord/10068",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "issue types",
"Created": "2025-05-20T09:12:29.155+0000",
"Object": "Task",
"Summary": "Issue type created",
"Type": "ISSUE_TYPE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Workflow created",
"FullyQualifiedName": "AuditRecord/10069",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "workflows",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Software workflow for project 10001",
"Created": "2025-05-20T09:12:29.792+0000",
"Object": "Software workflow for project 10001",
"Summary": "Workflow created",
"Type": "WORKFLOW"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field configuration scheme updated",
"FullyQualifiedName": "AuditRecord/10070",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "fields",
"ChangedField0_Name": "Issue Type",
"ChangedField0_To": "Epic",
"ChangedField1_Name": "Field Configuration",
"ChangedField1_To": "LEARNJIRA-10006",
"Created": "2025-05-20T09:12:30.456+0000",
"Object": "Field Configuration Scheme for Project LEARNJIRA",
"Summary": "Field configuration scheme updated",
"Type": "SCHEME"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Issue type created",
"FullyQualifiedName": "AuditRecord/10071",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "issue types",
"Created": "2025-05-20T09:12:30.746+0000",
"Object": "Epic",
"Summary": "Issue type created",
"Type": "ISSUE_TYPE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field configuration scheme updated",
"FullyQualifiedName": "AuditRecord/10072",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "fields",
"ChangedField0_Name": "Issue Type",
"ChangedField0_To": "Subtask",
"ChangedField1_Name": "Field Configuration",
"ChangedField1_To": "LEARNJIRA-10007",
"Created": "2025-05-20T09:12:31.363+0000",
"Object": "Field Configuration Scheme for Project LEARNJIRA",
"Summary": "Field configuration scheme updated",
"Type": "SCHEME"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Issue type created",
"FullyQualifiedName": "AuditRecord/10073",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "issue types",
"Created": "2025-05-20T09:12:31.461+0000",
"Object": "Subtask",
"Summary": "Issue type created",
"Type": "ISSUE_TYPE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project created",
"FullyQualifiedName": "AuditRecord/10074",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "d1e68c57-d711-4b14-a2c2-ef4bd13e60b9",
"AssociatedItem0_Type": "USER",
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "projects",
"ChangedField0_Name": "Name",
"ChangedField0_To": "(Learn) Jira Premium benefits in 5 min 👋",
"ChangedField1_Name": "Key",
"ChangedField1_To": "LEARNJIRA",
"ChangedField2_Name": "Description",
"ChangedField2_To": "",
"ChangedField3_Name": "Project lead",
"ChangedField3_To": "d1e68c57-d711-4b14-a2c2-ef4bd13e60b9",
"ChangedField4_Name": "Default Assignee",
"ChangedField4_To": "Unassigned",
"Created": "2025-05-20T09:12:31.995+0000",
"Object": "(Learn) Jira Premium benefits in 5 min 👋",
"Summary": "Project created",
"Type": "PROJECT"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10075",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Story Points",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Measurement of complexity and/or size of a requirement.",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Number Field",
"Created": "2025-05-20T09:12:34.016+0000",
"Object": "Story Points",
"RemoteAddress": "10.20.110.172",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10076",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "(Learn) Jira Premium benefits in 5 min 👋",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "60e5a86a471e61006a4c51fd",
"Created": "2025-05-20T09:16:46.236+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10077",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "My Scrum Project",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "60e5a86a471e61006a4c51fd",
"Created": "2025-05-20T09:16:46.289+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10078",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "60e5a86a471e61006a4c51fd",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:16:46.636+0000",
"Object": "jira-users-shaider",
"RemoteAddress": "10.26.66.143",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10079",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "(Learn) Jira Premium benefits in 5 min 👋",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 60e5a86a471e61006a4c51fd",
"Created": "2025-05-20T09:16:49.651+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10080",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "My Scrum Project",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 60e5a86a471e61006a4c51fd",
"Created": "2025-05-20T09:16:49.683+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10081",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:16:50.382+0000",
"Object": "jira-users-shaider",
"RemoteAddress": "10.26.66.143",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10082",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "(Learn) Jira Premium benefits in 5 min 👋",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "5b6c7b3afbc68529c6c47967, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 60e5a86a471e61006a4c51fd",
"Created": "2025-05-20T09:16:52.052+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10083",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "My Scrum Project",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5b6c7b3afbc68529c6c47967, 60e5a86a471e61006a4c51fd",
"Created": "2025-05-20T09:16:52.107+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10084",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "5b6c7b3afbc68529c6c47967",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:16:52.761+0000",
"Object": "jira-users-shaider",
"RemoteAddress": "10.26.66.143",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10085",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "(Learn) Jira Premium benefits in 5 min 👋",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "5b6c7b3afbc68529c6c47967, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 60e5a86a471e61006a4c51fd, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077",
"Created": "2025-05-20T09:16:55.592+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10086",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "My Scrum Project",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5b6c7b3afbc68529c6c47967, 60e5a86a471e61006a4c51fd, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077",
"Created": "2025-05-20T09:16:55.622+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10087",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "557058:f58131cb-b67d-43c7-b30d-6b58d40bd077",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:16:56.370+0000",
"Object": "jira-admins-shaider",
"RemoteAddress": "10.16.132.66",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10088",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "557058:f58131cb-b67d-43c7-b30d-6b58d40bd077",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:16:56.411+0000",
"Object": "jira-users-shaider",
"RemoteAddress": "10.16.132.66",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10089",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "(Learn) Jira Premium benefits in 5 min 👋",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "5b6c7b3afbc68529c6c47967, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 60e5a86a471e61006a4c51fd, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077, 5d53f3cbc6b9320d9ea5bdc2",
"Created": "2025-05-20T09:16:57.962+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10090",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "My Scrum Project",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5b6c7b3afbc68529c6c47967, 60e5a86a471e61006a4c51fd, 5d53f3cbc6b9320d9ea5bdc2, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077",
"Created": "2025-05-20T09:16:57.988+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10091",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "5d53f3cbc6b9320d9ea5bdc2",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:16:58.595+0000",
"Object": "jira-users-shaider",
"RemoteAddress": "10.26.105.37",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10092",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "(Learn) Jira Premium benefits in 5 min 👋",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "5b6c7b3afbc68529c6c47967, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 60e5a86a471e61006a4c51fd, 5cb4ae0e4b97ab11a18e00c7, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077, 5d53f3cbc6b9320d9ea5bdc2",
"Created": "2025-05-20T09:17:00.720+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10093",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "My Scrum Project",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "5cb4ae0e4b97ab11a18e00c7, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5b6c7b3afbc68529c6c47967, 60e5a86a471e61006a4c51fd, 5d53f3cbc6b9320d9ea5bdc2, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077",
"Created": "2025-05-20T09:17:00.751+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10094",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "5cb4ae0e4b97ab11a18e00c7",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:17:01.225+0000",
"Object": "jira-admins-shaider",
"RemoteAddress": "10.16.132.66",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10095",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "5cb4ae0e4b97ab11a18e00c7",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:17:01.927+0000",
"Object": "jira-users-shaider",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10096",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "(Learn) Jira Premium benefits in 5 min 👋",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "5b6c7b3afbc68529c6c47967, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5cb4ae0e4b97ab11a18e00c7, 60e5a86a471e61006a4c51fd, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077, 5d53f3cbc6b9320d9ea5bdc2, 557058:0867a421-a9ee-4659-801a-bc0ee4a4487e",
"Created": "2025-05-20T09:17:03.279+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10097",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "My Scrum Project",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "5cb4ae0e4b97ab11a18e00c7, 557058:0867a421-a9ee-4659-801a-bc0ee4a4487e, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5b6c7b3afbc68529c6c47967, 60e5a86a471e61006a4c51fd, 5d53f3cbc6b9320d9ea5bdc2, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077",
"Created": "2025-05-20T09:17:03.306+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10098",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "557058:0867a421-a9ee-4659-801a-bc0ee4a4487e",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:17:03.773+0000",
"Object": "jira-users-shaider",
"RemoteAddress": "10.26.105.37",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10099",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "(Learn) Jira Premium benefits in 5 min 👋",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "5b6c7b3afbc68529c6c47967, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5cb4ae0e4b97ab11a18e00c7, 60e5a86a471e61006a4c51fd, 557058:950f9f5b-3d6d-4e1d-954a-21367ae9ac75, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077, 5d53f3cbc6b9320d9ea5bdc2, 557058:0867a421-a9ee-4659-801a-bc0ee4a4487e",
"Created": "2025-05-20T09:17:06.320+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10100",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "My Scrum Project",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "5cb4ae0e4b97ab11a18e00c7, 557058:950f9f5b-3d6d-4e1d-954a-21367ae9ac75, 557058:0867a421-a9ee-4659-801a-bc0ee4a4487e, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5b6c7b3afbc68529c6c47967, 60e5a86a471e61006a4c51fd, 5d53f3cbc6b9320d9ea5bdc2, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077",
"Created": "2025-05-20T09:17:06.357+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10101",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "a71b105a-ea02-4b84-a162-64b8abccda81",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:17:06.383+0000",
"Object": "atlassian-addons-admin",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10102",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "a71b105a-ea02-4b84-a162-64b8abccda81",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T09:17:06.386+0000",
"Object": "a71b105a-ea02-4b84-a162-64b8abccda81",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10103",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "a71b105a-ea02-4b84-a162-64b8abccda81",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:17:06.821+0000",
"Object": "jira-users-shaider",
"RemoteAddress": "10.26.72.216",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10104",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "a71b105a-ea02-4b84-a162-64b8abccda81",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:17:07.034+0000",
"Object": "jira-admins-shaider",
"RemoteAddress": "10.16.132.66",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10105",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "(Learn) Jira Premium benefits in 5 min 👋",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "5b6c7b3afbc68529c6c47967, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5cb4ae0e4b97ab11a18e00c7, 557058:950f9f5b-3d6d-4e1d-954a-21367ae9ac75, 60e5a86a471e61006a4c51fd, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077, 5d53f3cbc6b9320d9ea5bdc2, 5dd64082af96bc0efbe55103, 557058:0867a421-a9ee-4659-801a-bc0ee4a4487e",
"Created": "2025-05-20T09:17:08.228+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10106",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "My Scrum Project",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "5cb4ae0e4b97ab11a18e00c7, 557058:950f9f5b-3d6d-4e1d-954a-21367ae9ac75, 557058:0867a421-a9ee-4659-801a-bc0ee4a4487e, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5b6c7b3afbc68529c6c47967, 60e5a86a471e61006a4c51fd, 5d53f3cbc6b9320d9ea5bdc2, 5dd64082af96bc0efbe55103, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077",
"Created": "2025-05-20T09:17:08.256+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10107",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "c6a62781-e4f9-484c-baa8-0ee189f25039",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T09:17:08.450+0000",
"Object": "c6a62781-e4f9-484c-baa8-0ee189f25039",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10108",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "c6a62781-e4f9-484c-baa8-0ee189f25039",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:17:09.139+0000",
"Object": "jira-users-shaider",
"RemoteAddress": "10.26.66.143",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10109",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "e89f88c7-0ba9-4908-b2cc-b45f11ec5ca7",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T09:17:11.866+0000",
"Object": "e89f88c7-0ba9-4908-b2cc-b45f11ec5ca7",
"RemoteAddress": "10.26.72.216",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10110",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "(Learn) Jira Premium benefits in 5 min 👋",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "63a22fb348b367d78a14c15b, 5b6c7b3afbc68529c6c47967, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5cb4ae0e4b97ab11a18e00c7, 557058:950f9f5b-3d6d-4e1d-954a-21367ae9ac75, 60e5a86a471e61006a4c51fd, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077, 5d53f3cbc6b9320d9ea5bdc2, 5dd64082af96bc0efbe55103, 557058:0867a421-a9ee-4659-801a-bc0ee4a4487e",
"Created": "2025-05-20T09:17:11.960+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10111",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "My Scrum Project",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "63a22fb348b367d78a14c15b, 5cb4ae0e4b97ab11a18e00c7, 557058:950f9f5b-3d6d-4e1d-954a-21367ae9ac75, 557058:0867a421-a9ee-4659-801a-bc0ee4a4487e, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5b6c7b3afbc68529c6c47967, 60e5a86a471e61006a4c51fd, 5d53f3cbc6b9320d9ea5bdc2, 5dd64082af96bc0efbe55103, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077",
"Created": "2025-05-20T09:17:11.992+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User removed from group",
"FullyQualifiedName": "AuditRecord/10112",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "e89f88c7-0ba9-4908-b2cc-b45f11ec5ca7",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:17:12.169+0000",
"Object": "atlassian-addons-admin",
"Summary": "User removed from group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10113",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "e89f88c7-0ba9-4908-b2cc-b45f11ec5ca7",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T09:17:12.191+0000",
"Object": "e89f88c7-0ba9-4908-b2cc-b45f11ec5ca7",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10114",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "e89f88c7-0ba9-4908-b2cc-b45f11ec5ca7",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:17:12.446+0000",
"Object": "atlassian-addons-admin",
"RemoteAddress": "10.16.132.66",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10115",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "e89f88c7-0ba9-4908-b2cc-b45f11ec5ca7",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:17:12.511+0000",
"Object": "jira-admins-shaider",
"RemoteAddress": "10.16.132.66",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10116",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "e89f88c7-0ba9-4908-b2cc-b45f11ec5ca7",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:17:12.644+0000",
"Object": "jira-users-shaider",
"RemoteAddress": "10.26.66.143",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10117",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "(Learn) Jira Premium benefits in 5 min 👋",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "63a22fb348b367d78a14c15b, 5b6c7b3afbc68529c6c47967, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5cb4ae0e4b97ab11a18e00c7, 557058:950f9f5b-3d6d-4e1d-954a-21367ae9ac75, 60e5a86a471e61006a4c51fd, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077, 5d53f3cbc6b9320d9ea5bdc2, 5dd64082af96bc0efbe55103, 557058:0867a421-a9ee-4659-801a-bc0ee4a4487e, 5cf112d31552030f1e3a5905",
"Created": "2025-05-20T09:17:14.744+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10118",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "My Scrum Project",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "63a22fb348b367d78a14c15b, 5cb4ae0e4b97ab11a18e00c7, 557058:950f9f5b-3d6d-4e1d-954a-21367ae9ac75, 557058:0867a421-a9ee-4659-801a-bc0ee4a4487e, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5b6c7b3afbc68529c6c47967, 60e5a86a471e61006a4c51fd, 5cf112d31552030f1e3a5905, 5d53f3cbc6b9320d9ea5bdc2, 5dd64082af96bc0efbe55103, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077",
"Created": "2025-05-20T09:17:14.775+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10119",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "5cf112d31552030f1e3a5905",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:17:15.565+0000",
"Object": "jira-users-shaider",
"RemoteAddress": "10.16.149.189",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10120",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "(Learn) Jira Premium benefits in 5 min 👋",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "63a22fb348b367d78a14c15b, 5b6c7b3afbc68529c6c47967, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5cb4ae0e4b97ab11a18e00c7, 557058:950f9f5b-3d6d-4e1d-954a-21367ae9ac75, 60e5a86a471e61006a4c51fd, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077, 5d53f3cbc6b9320d9ea5bdc2, 5dd64082af96bc0efbe55103, 557058:0867a421-a9ee-4659-801a-bc0ee4a4487e, 5cf112d31552030f1e3a5905, 630db2cd9796033b256bc349",
"Created": "2025-05-20T09:17:18.574+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Project roles changed",
"FullyQualifiedName": "AuditRecord/10121",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "My Scrum Project",
"AssociatedItem0_Type": "PROJECT",
"Category": "projects",
"ChangedField0_Name": "Users",
"ChangedField0_To": "63a22fb348b367d78a14c15b, 5cb4ae0e4b97ab11a18e00c7, 557058:950f9f5b-3d6d-4e1d-954a-21367ae9ac75, 557058:0867a421-a9ee-4659-801a-bc0ee4a4487e, 630db2cd9796033b256bc349, 557058:214cdd6a-ff93-4d8b-838b-62dfcf1a2a71, 5b6c7b3afbc68529c6c47967, 60e5a86a471e61006a4c51fd, 5cf112d31552030f1e3a5905, 5d53f3cbc6b9320d9ea5bdc2, 5dd64082af96bc0efbe55103, 557058:f58131cb-b67d-43c7-b30d-6b58d40bd077",
"Created": "2025-05-20T09:17:18.605+0000",
"Object": "atlassian-addons-project-access",
"Summary": "Project roles changed",
"Type": "PROJECT_ROLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10122",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "630db2cd9796033b256bc349",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:17:19.009+0000",
"Object": "jira-admins-shaider",
"RemoteAddress": "10.26.66.143",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10123",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "630db2cd9796033b256bc349",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-20T09:17:19.052+0000",
"Object": "jira-users-shaider",
"RemoteAddress": "10.16.132.66",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field context created",
"FullyQualifiedName": "AuditRecord/10124",
"Type": "AuditRecord",
"Metadata": {
"Category": "custom field context",
"ChangedField0_Name": "Associated to project(s)",
"ChangedField0_To": "All projects",
"ChangedField1_Name": "Associated to issue type(s)",
"ChangedField1_To": "All issue types",
"Created": "2025-05-20T09:17:19.186+0000",
"Object": "com.atlassian.atlas.jira__project-key",
"Summary": "Field context created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10125",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Project overview key",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Key of project overview connected via Atlassian Home for Jira Cloud",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Project overview key",
"Created": "2025-05-20T09:17:19.206+0000",
"Object": "Project overview key",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Field context created",
"FullyQualifiedName": "AuditRecord/10126",
"Type": "AuditRecord",
"Metadata": {
"Category": "custom field context",
"ChangedField0_Name": "Associated to project(s)",
"ChangedField0_To": "All projects",
"ChangedField1_Name": "Associated to issue type(s)",
"ChangedField1_To": "All issue types",
"Created": "2025-05-20T09:17:19.372+0000",
"Object": "com.atlassian.atlas.jira__project-status",
"Summary": "Field context created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Custom field created",
"FullyQualifiedName": "AuditRecord/10127",
"Type": "AuditRecord",
"Metadata": {
"Category": "fields",
"ChangedField0_Name": "Name",
"ChangedField0_To": "Project overview status",
"ChangedField1_Name": "Description",
"ChangedField1_To": "Status of project overview connected via Atlassian Home for Jira Cloud",
"ChangedField2_Name": "Type",
"ChangedField2_To": "Project overview status",
"Created": "2025-05-20T09:17:19.389+0000",
"Object": "Project overview status",
"Summary": "Custom field created",
"Type": "CUSTOM_FIELD"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10160",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "cbefd655-3c82-41d7-993b-1e39524f1a70",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:16.406+0000",
"Object": "cbefd655-3c82-41d7-993b-1e39524f1a70",
"RemoteAddress": "10.26.109.230",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10161",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "127e0d88-4090-4621-9ba6-e821b3337030",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:18.580+0000",
"Object": "127e0d88-4090-4621-9ba6-e821b3337030",
"RemoteAddress": "10.26.69.20",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10162",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "fd5c50d8-5ffe-45a5-8f0f-710d02cfd080",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:19.363+0000",
"Object": "fd5c50d8-5ffe-45a5-8f0f-710d02cfd080",
"RemoteAddress": "10.26.124.174",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10163",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "c0183bc4-5673-42a3-a769-1c91715c92c6",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:20.260+0000",
"Object": "c0183bc4-5673-42a3-a769-1c91715c92c6",
"RemoteAddress": "10.26.124.174",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10164",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "08251f11-274a-43e5-8672-dd60e972654a",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:21.481+0000",
"Object": "08251f11-274a-43e5-8672-dd60e972654a",
"RemoteAddress": "10.16.140.214",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10165",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "1c0e598f-5a5e-49bf-b0aa-6251ac808027",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:22.698+0000",
"Object": "1c0e598f-5a5e-49bf-b0aa-6251ac808027",
"RemoteAddress": "10.26.124.174",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10166",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "72f57329-34af-40e2-a217-40128d049fa9",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:24.162+0000",
"Object": "72f57329-34af-40e2-a217-40128d049fa9",
"RemoteAddress": "10.26.72.252",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10167",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "b73e79da-3732-42dc-98f8-5cfe2765fbcf",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:25.105+0000",
"Object": "b73e79da-3732-42dc-98f8-5cfe2765fbcf",
"RemoteAddress": "10.26.72.223",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10168",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "dd62e8db-f985-461c-8957-630cba36dca3",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:25.787+0000",
"Object": "dd62e8db-f985-461c-8957-630cba36dca3",
"RemoteAddress": "10.26.69.20",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10169",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "95850660-2865-498a-a78c-ea40add0f257",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:27.277+0000",
"Object": "95850660-2865-498a-a78c-ea40add0f257",
"RemoteAddress": "10.26.124.174",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10170",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "2b683cd5-f6b2-4d41-8383-6987df3c5337",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:28.110+0000",
"Object": "2b683cd5-f6b2-4d41-8383-6987df3c5337",
"RemoteAddress": "10.26.109.230",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10171",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "55e4b181-4a42-45ed-bcc3-0c0576b8a709",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:29.603+0000",
"Object": "55e4b181-4a42-45ed-bcc3-0c0576b8a709",
"RemoteAddress": "10.26.72.252",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10172",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "ec58c96c-2129-4781-81d9-199189807ea5",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:31.305+0000",
"Object": "ec58c96c-2129-4781-81d9-199189807ea5",
"RemoteAddress": "10.16.130.180",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10173",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "ef289a7b-3601-4d0d-903c-eee4a53695b5",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:31.429+0000",
"Object": "ef289a7b-3601-4d0d-903c-eee4a53695b5",
"RemoteAddress": "10.26.69.20",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10174",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "adbe0dd1-5afb-44cb-a662-16151eaa2ee2",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:32.418+0000",
"Object": "adbe0dd1-5afb-44cb-a662-16151eaa2ee2",
"RemoteAddress": "10.26.72.252",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10175",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "491b809f-064b-48a2-9d98-8940cce0fa81",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:34.218+0000",
"Object": "491b809f-064b-48a2-9d98-8940cce0fa81",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10176",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "0b0ad180-899d-4f27-a526-ce558ee2b454",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:34.807+0000",
"Object": "0b0ad180-899d-4f27-a526-ce558ee2b454",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10177",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "ef5f609c-8acd-4cdd-a867-caea2cb04201",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:36.413+0000",
"Object": "ef5f609c-8acd-4cdd-a867-caea2cb04201",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10178",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "a47aa1f1-5663-48cc-ab44-50a0a18f7efd",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:37.039+0000",
"Object": "a47aa1f1-5663-48cc-ab44-50a0a18f7efd",
"RemoteAddress": "10.26.72.252",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10179",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "3696b761-cc6f-481e-9a33-08b3367b7bce",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:38.456+0000",
"Object": "3696b761-cc6f-481e-9a33-08b3367b7bce",
"RemoteAddress": "10.16.142.95",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10180",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "15439766-f128-4fcb-b55a-bce2d3179d6c",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:40.015+0000",
"Object": "15439766-f128-4fcb-b55a-bce2d3179d6c",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10181",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "d4576f35-c3fc-466e-afaa-24dca7167c91",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:40.500+0000",
"Object": "d4576f35-c3fc-466e-afaa-24dca7167c91",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10182",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "73a7eb7c-322b-449c-9a9b-4eca296ac805",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:41.451+0000",
"Object": "73a7eb7c-322b-449c-9a9b-4eca296ac805",
"RemoteAddress": "10.16.140.214",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10183",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "bd41f0b5-f733-47f1-bae7-ab71a923f129",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:42.752+0000",
"Object": "bd41f0b5-f733-47f1-bae7-ab71a923f129",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10184",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "6fbf8958-93f4-4cb4-9bcc-8f3756bcd2c2",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:44.141+0000",
"Object": "6fbf8958-93f4-4cb4-9bcc-8f3756bcd2c2",
"RemoteAddress": "10.26.124.174",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10185",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "06425cbc-036a-4da4-bcb9-659f0e1478cf",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:45.192+0000",
"Object": "06425cbc-036a-4da4-bcb9-659f0e1478cf",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10186",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "6fa0ac8d-61b5-4954-b81f-8d32a9c3ac30",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:45.976+0000",
"Object": "6fa0ac8d-61b5-4954-b81f-8d32a9c3ac30",
"RemoteAddress": "10.26.69.20",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10187",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "3d59bbf5-af46-4cc4-8dd5-7869dd05a0bd",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:47.373+0000",
"Object": "3d59bbf5-af46-4cc4-8dd5-7869dd05a0bd",
"RemoteAddress": "10.26.113.166",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10188",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "e842d31a-bc10-4448-af5f-db6bf6999f7e",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:48.431+0000",
"Object": "e842d31a-bc10-4448-af5f-db6bf6999f7e",
"RemoteAddress": "10.26.124.174",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10189",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "05c895af-5335-46ca-9c2d-2a73e2b360a8",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:49.654+0000",
"Object": "05c895af-5335-46ca-9c2d-2a73e2b360a8",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10190",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "bbba649f-2d48-4661-82c0-dfa3393a3215",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:51.717+0000",
"Object": "bbba649f-2d48-4661-82c0-dfa3393a3215",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10191",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "a21b45de-4a21-4c77-a920-6cb658e0d2d5",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:51.735+0000",
"Object": "a21b45de-4a21-4c77-a920-6cb658e0d2d5",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10192",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "f956eab6-78f6-4bde-918d-23273d2674ec",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:52.550+0000",
"Object": "f956eab6-78f6-4bde-918d-23273d2674ec",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10193",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "f1fb816a-9862-4424-80bc-c6d8503efd96",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:53.870+0000",
"Object": "f1fb816a-9862-4424-80bc-c6d8503efd96",
"RemoteAddress": "10.26.69.20",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10194",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "5788ffcd-e20c-43e5-b4e1-af981de34331",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:55.333+0000",
"Object": "5788ffcd-e20c-43e5-b4e1-af981de34331",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10195",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "b25c7d8e-dee0-4211-8d81-554a49bf29e6",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:56.566+0000",
"Object": "b25c7d8e-dee0-4211-8d81-554a49bf29e6",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10196",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "d6257f38-b615-47ac-ba65-a87cf6a2b862",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:57.923+0000",
"Object": "d6257f38-b615-47ac-ba65-a87cf6a2b862",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10197",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "d651c2e1-ff94-4f17-9238-0b428a652875",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:56:58.830+0000",
"Object": "d651c2e1-ff94-4f17-9238-0b428a652875",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10198",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "cb3435fb-aa28-47bf-b4ec-74639485ccd1",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-20T19:57:00.662+0000",
"Object": "cb3435fb-aa28-47bf-b4ec-74639485ccd1",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Issue type scheme updated",
"FullyQualifiedName": "AuditRecord/10226",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "issue type scheme",
"Created": "2025-05-26T10:19:43.125+0000",
"Object": "Default Issue Type Scheme",
"RemoteAddress": "10.20.41.84",
"Summary": "Issue type scheme updated",
"Type": "ISSUE_TYPE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Issue type created",
"FullyQualifiedName": "AuditRecord/10227",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "issue types",
"Created": "2025-05-26T10:19:43.153+0000",
"Object": "Story",
"RemoteAddress": "10.20.41.84",
"Summary": "Issue type created",
"Type": "ISSUE_TYPE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Sprint created",
"FullyQualifiedName": "AuditRecord/10228",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "sprints",
"Created": "2025-05-26T10:41:38.965+0000",
"Object": "SCRUM Sprint 2",
"RemoteAddress": "10.20.108.78",
"Summary": "Sprint created",
"Type": "SPRINT"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Sprint deleted",
"FullyQualifiedName": "AuditRecord/10229",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "sprints",
"Created": "2025-05-26T10:42:18.259+0000",
"Object": "SCRUM Sprint 2",
"RemoteAddress": "10.20.41.84",
"Summary": "Sprint deleted",
"Type": "SPRINT"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Sprint started",
"FullyQualifiedName": "AuditRecord/10230",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "sprints",
"Created": "2025-05-26T10:44:01.559+0000",
"Object": "SCRUM Sprint 1",
"RemoteAddress": "10.22.111.74",
"Summary": "Sprint started",
"Type": "SPRINT"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "Sprint updated",
"FullyQualifiedName": "AuditRecord/10231",
"Type": "AuditRecord",
"Metadata": {
"AuthorAccountID": "712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Category": "sprints",
"Created": "2025-05-26T10:44:01.564+0000",
"Object": "SCRUM Sprint 1",
"RemoteAddress": "10.22.111.74",
"Summary": "Sprint updated",
"Type": "SPRINT"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User created",
"FullyQualifiedName": "AuditRecord/10259",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "cf5b456d-6059-48dc-995a-04e18681dc21",
"AssociatedItem0_Type": "USER",
"Category": "user management",
"ChangedField0_Name": "Active / Inactive",
"ChangedField0_To": "Active",
"Created": "2025-05-29T11:12:19.464+0000",
"Object": "cf5b456d-6059-48dc-995a-04e18681dc21",
"Summary": "User created",
"Type": "USER"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "User added to group",
"FullyQualifiedName": "AuditRecord/10260",
"Type": "AuditRecord",
"Metadata": {
"AssociatedItem0_Name": "cf5b456d-6059-48dc-995a-04e18681dc21",
"AssociatedItem0_Type": "USER",
"Category": "group management",
"Created": "2025-05-29T11:12:19.567+0000",
"Object": "jira-users-shaider",
"Summary": "User added to group",
"Type": "GROUP"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM board",
"FullyQualifiedName": "Board/1",
"Type": "Board",
"Metadata": {
"AvatarURI": "https://shaider.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10425?size=small",
"BoardSelfURL": "https://shaider.atlassian.net/rest/agile/1.0/board/1",
"BoardType": "simple",
"DisplayName": "My Scrum Project (SCRUM)",
"IsPrivate": "false",
"ProjectID": "10000",
"ProjectKey": "SCRUM",
"ProjectName": "My Scrum Project",
"ProjectType": "software"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM board",
"FullyQualifiedName": "Board/1",
"Type": "Board",
"Metadata": {
"AvatarURI": "https://shaider.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10425?size=small",
"BoardSelfURL": "https://shaider.atlassian.net/rest/agile/1.0/board/1",
"BoardType": "simple",
"DisplayName": "My Scrum Project (SCRUM)",
"IsPrivate": "false",
"ProjectID": "10000",
"ProjectKey": "SCRUM",
"ProjectName": "My Scrum Project",
"ProjectType": "software"
},
"Parent": null
},
"Permission": {
"Value": "browse_projects",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM board",
"FullyQualifiedName": "Board/1",
"Type": "Board",
"Metadata": {
"AvatarURI": "https://shaider.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10425?size=small",
"BoardSelfURL": "https://shaider.atlassian.net/rest/agile/1.0/board/1",
"BoardType": "simple",
"DisplayName": "My Scrum Project (SCRUM)",
"IsPrivate": "false",
"ProjectID": "10000",
"ProjectKey": "SCRUM",
"ProjectName": "My Scrum Project",
"ProjectType": "software"
},
"Parent": null
},
"Permission": {
"Value": "manage_sprints_permission",
"Parent": null
}
},
{
"Resource": {
"Name": "(Learn) Jira Premium benefits in 5 min",
"FullyQualifiedName": "Board/2",
"Type": "Board",
"Metadata": {
"AvatarURI": "https://shaider.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10411?size=small",
"BoardSelfURL": "https://shaider.atlassian.net/rest/agile/1.0/board/2",
"BoardType": "simple",
"DisplayName": "(Learn) Jira Premium benefits in 5 min 👋 (LEARNJIRA)",
"IsPrivate": "false",
"ProjectID": "10001",
"ProjectKey": "LEARNJIRA",
"ProjectName": "(Learn) Jira Premium benefits in 5 min 👋",
"ProjectType": "software"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "(Learn) Jira Premium benefits in 5 min",
"FullyQualifiedName": "Board/2",
"Type": "Board",
"Metadata": {
"AvatarURI": "https://shaider.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10411?size=small",
"BoardSelfURL": "https://shaider.atlassian.net/rest/agile/1.0/board/2",
"BoardType": "simple",
"DisplayName": "(Learn) Jira Premium benefits in 5 min 👋 (LEARNJIRA)",
"IsPrivate": "false",
"ProjectID": "10001",
"ProjectKey": "LEARNJIRA",
"ProjectName": "(Learn) Jira Premium benefits in 5 min 👋",
"ProjectType": "software"
},
"Parent": null
},
"Permission": {
"Value": "browse_projects",
"Parent": null
}
},
{
"Resource": {
"Name": "(Learn) Jira Premium benefits in 5 min",
"FullyQualifiedName": "Board/2",
"Type": "Board",
"Metadata": {
"AvatarURI": "https://shaider.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10411?size=small",
"BoardSelfURL": "https://shaider.atlassian.net/rest/agile/1.0/board/2",
"BoardType": "simple",
"DisplayName": "(Learn) Jira Premium benefits in 5 min 👋 (LEARNJIRA)",
"IsPrivate": "false",
"ProjectID": "10001",
"ProjectKey": "LEARNJIRA",
"ProjectName": "(Learn) Jira Premium benefits in 5 min 👋",
"ProjectType": "software"
},
"Parent": null
},
"Permission": {
"Value": "manage_sprints_permission",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "add_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "assign_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "close_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "create_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "create_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "delete_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "edit_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "link_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "manage_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "modify_reporter",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "move_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "resolve_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "schedule_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "set_issue_security",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "transition_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "unarchive_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "view_voters_and_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-2",
"FullyQualifiedName": "Epic/10034",
"Type": "Epic",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Notification Service"
},
"Parent": null
},
"Permission": {
"Value": "work_on_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "system-administrators",
"FullyQualifiedName": "Group/183f64ca-3ef5-469a-82cb-a84da8b11cbd",
"Type": "Group",
"Metadata": {
"HTML": "system-administrators"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "jira-users-shaider",
"FullyQualifiedName": "Group/6b44ab2c-c950-4bd7-8e51-a0e35ea95bce",
"Type": "Group",
"Metadata": {
"HTML": "jira-users-shaider",
"Label0_Text": "Jira Software",
"Label0_Title": "Users added to this group will be given access to \u003cstrong\u003eJira Software\u003c/strong\u003e",
"Label0_Type": "SINGLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "jira-user-access-admins-shaider",
"FullyQualifiedName": "Group/7c9b5ee2-fe05-4b6e-9c88-d6d4887caf7c",
"Type": "Group",
"Metadata": {
"HTML": "jira-user-access-admins-shaider"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "jira-admins-shaider",
"FullyQualifiedName": "Group/e06e77c1-dea1-42ba-a119-ed1189826165",
"Type": "Group",
"Metadata": {
"HTML": "jira-admins-shaider",
"Label0_Text": "Admin",
"Label0_Title": "Users added to this group will be given administrative access",
"Label0_Type": "ADMIN"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "atlassian-addons-admin",
"FullyQualifiedName": "Group/f04dd022-c413-4790-aed4-0c7b5167ec31",
"Type": "Group",
"Metadata": {
"HTML": "atlassian-addons-admin"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "org-admins",
"FullyQualifiedName": "Group/f94760f8-4a5e-49da-ac1e-0d4281e86aa1",
"Type": "Group",
"Metadata": {
"HTML": "org-admins",
"Label0_Text": "Admin",
"Label0_Title": "Users added to this group will be given administrative access",
"Label0_Type": "ADMIN",
"Label1_Text": "Jira Software",
"Label1_Title": "Users added to this group will be given access to \u003cstrong\u003eJira Software\u003c/strong\u003e",
"Label1_Type": "SINGLE"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "My Scrum Project",
"FullyQualifiedName": "Project/10000",
"Type": "Project",
"Metadata": {
"Key": "SCRUM",
"Private": "false",
"TypeKey": "software",
"UUID": "6fc39a42-a49a-4ba6-8fe0-d0e23787eb39"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "My Scrum Project",
"FullyQualifiedName": "Project/10000",
"Type": "Project",
"Metadata": {
"Key": "SCRUM",
"Private": "false",
"TypeKey": "software",
"UUID": "6fc39a42-a49a-4ba6-8fe0-d0e23787eb39"
},
"Parent": null
},
"Permission": {
"Value": "administer_projects",
"Parent": null
}
},
{
"Resource": {
"Name": "My Scrum Project",
"FullyQualifiedName": "Project/10000",
"Type": "Project",
"Metadata": {
"Key": "SCRUM",
"Private": "false",
"TypeKey": "software",
"UUID": "6fc39a42-a49a-4ba6-8fe0-d0e23787eb39"
},
"Parent": null
},
"Permission": {
"Value": "browse_projects",
"Parent": null
}
},
{
"Resource": {
"Name": "My Scrum Project",
"FullyQualifiedName": "Project/10000",
"Type": "Project",
"Metadata": {
"Key": "SCRUM",
"Private": "false",
"TypeKey": "software",
"UUID": "6fc39a42-a49a-4ba6-8fe0-d0e23787eb39"
},
"Parent": null
},
"Permission": {
"Value": "create_project",
"Parent": null
}
},
{
"Resource": {
"Name": "My Scrum Project",
"FullyQualifiedName": "Project/10000",
"Type": "Project",
"Metadata": {
"Key": "SCRUM",
"Private": "false",
"TypeKey": "software",
"UUID": "6fc39a42-a49a-4ba6-8fe0-d0e23787eb39"
},
"Parent": null
},
"Permission": {
"Value": "edit_issue_layout",
"Parent": null
}
},
{
"Resource": {
"Name": "My Scrum Project",
"FullyQualifiedName": "Project/10000",
"Type": "Project",
"Metadata": {
"Key": "SCRUM",
"Private": "false",
"TypeKey": "software",
"UUID": "6fc39a42-a49a-4ba6-8fe0-d0e23787eb39"
},
"Parent": null
},
"Permission": {
"Value": "view_dev_tools",
"Parent": null
}
},
{
"Resource": {
"Name": "(Learn) Jira Premium benefits in 5 min 👋",
"FullyQualifiedName": "Project/10001",
"Type": "Project",
"Metadata": {
"Key": "LEARNJIRA",
"Private": "false",
"TypeKey": "software",
"UUID": "05b93f07-2de4-4a25-82ce-0ec40b666b3b"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "(Learn) Jira Premium benefits in 5 min 👋",
"FullyQualifiedName": "Project/10001",
"Type": "Project",
"Metadata": {
"Key": "LEARNJIRA",
"Private": "false",
"TypeKey": "software",
"UUID": "05b93f07-2de4-4a25-82ce-0ec40b666b3b"
},
"Parent": null
},
"Permission": {
"Value": "administer_projects",
"Parent": null
}
},
{
"Resource": {
"Name": "(Learn) Jira Premium benefits in 5 min 👋",
"FullyQualifiedName": "Project/10001",
"Type": "Project",
"Metadata": {
"Key": "LEARNJIRA",
"Private": "false",
"TypeKey": "software",
"UUID": "05b93f07-2de4-4a25-82ce-0ec40b666b3b"
},
"Parent": null
},
"Permission": {
"Value": "browse_projects",
"Parent": null
}
},
{
"Resource": {
"Name": "(Learn) Jira Premium benefits in 5 min 👋",
"FullyQualifiedName": "Project/10001",
"Type": "Project",
"Metadata": {
"Key": "LEARNJIRA",
"Private": "false",
"TypeKey": "software",
"UUID": "05b93f07-2de4-4a25-82ce-0ec40b666b3b"
},
"Parent": null
},
"Permission": {
"Value": "create_project",
"Parent": null
}
},
{
"Resource": {
"Name": "(Learn) Jira Premium benefits in 5 min 👋",
"FullyQualifiedName": "Project/10001",
"Type": "Project",
"Metadata": {
"Key": "LEARNJIRA",
"Private": "false",
"TypeKey": "software",
"UUID": "05b93f07-2de4-4a25-82ce-0ec40b666b3b"
},
"Parent": null
},
"Permission": {
"Value": "edit_issue_layout",
"Parent": null
}
},
{
"Resource": {
"Name": "(Learn) Jira Premium benefits in 5 min 👋",
"FullyQualifiedName": "Project/10001",
"Type": "Project",
"Metadata": {
"Key": "LEARNJIRA",
"Private": "false",
"TypeKey": "software",
"UUID": "05b93f07-2de4-4a25-82ce-0ec40b666b3b"
},
"Parent": null
},
"Permission": {
"Value": "view_dev_tools",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "add_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "assign_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "close_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "create_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "create_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "delete_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "edit_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "link_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "manage_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "modify_reporter",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "move_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "resolve_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "schedule_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "set_issue_security",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "transition_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "unarchive_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "view_voters_and_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-6",
"FullyQualifiedName": "Story/10038",
"Type": "Story",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Story"
},
"Parent": null
},
"Permission": {
"Value": "work_on_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "add_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "assign_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "close_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "create_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "create_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "delete_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "edit_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "link_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "manage_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "modify_reporter",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "move_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "resolve_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "schedule_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "set_issue_security",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "transition_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "unarchive_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "view_voters_and_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-5",
"FullyQualifiedName": "Subtask/10037",
"Type": "Subtask",
"Metadata": {
"Project": "SCRUM",
"Status": "In Progress",
"Summary": "first sub-task"
},
"Parent": null
},
"Permission": {
"Value": "work_on_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "add_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "assign_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "close_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "create_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "create_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "delete_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "edit_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "link_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "manage_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "modify_reporter",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "move_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "resolve_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "schedule_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "set_issue_security",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "transition_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "unarchive_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "view_voters_and_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-1",
"FullyQualifiedName": "Task/10000",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Security \u0026 permissions: How to control who can edit or or manage projects 🚧"
},
"Parent": null
},
"Permission": {
"Value": "work_on_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "add_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "assign_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "close_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "create_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "create_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "delete_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "edit_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "link_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "manage_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "modify_reporter",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "move_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "resolve_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "schedule_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "set_issue_security",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "transition_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "unarchive_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "view_voters_and_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-2",
"FullyQualifiedName": "Task/10001",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Plans: How to use detailed roadmaps to plan out your work 📍"
},
"Parent": null
},
"Permission": {
"Value": "work_on_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "add_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "assign_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "close_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "create_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "create_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "delete_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "edit_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "link_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "manage_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "modify_reporter",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "move_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "resolve_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "schedule_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "set_issue_security",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "transition_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "unarchive_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "view_voters_and_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "LEARNJIRA-3",
"FullyQualifiedName": "Task/10002",
"Type": "Task",
"Metadata": {
"Project": "LEARNJIRA",
"Status": "To Do",
"Summary": "Atlassian Intelligence: How to work smarter with AI 🤖"
},
"Parent": null
},
"Permission": {
"Value": "work_on_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "add_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "assign_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "close_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "create_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "create_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "delete_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "edit_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "link_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "manage_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "modify_reporter",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "move_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "resolve_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "schedule_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "set_issue_security",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "transition_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "unarchive_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "view_voters_and_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-1",
"FullyQualifiedName": "Task/10033",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "My First Task"
},
"Parent": null
},
"Permission": {
"Value": "work_on_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "add_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "assign_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "close_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "create_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "create_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "delete_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "edit_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "link_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "manage_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "modify_reporter",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "move_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "resolve_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "schedule_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "set_issue_security",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "transition_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "unarchive_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "view_voters_and_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-3",
"FullyQualifiedName": "Task/10035",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "backend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "work_on_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "add_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "assign_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "close_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "create_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "create_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "delete_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "edit_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "link_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "manage_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "modify_reporter",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "move_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "resolve_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "schedule_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "set_issue_security",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "transition_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "unarchive_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "view_voters_and_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-4",
"FullyQualifiedName": "Task/10036",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "frontend functionality for Notification feature"
},
"Parent": null
},
"Permission": {
"Value": "work_on_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "add_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "administer",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "assign_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "close_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "create_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "create_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "delete_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "delete_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_attachments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "delete_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "edit_all_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "edit_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_comments",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "edit_own_worklogs",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "link_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "manage_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "modify_reporter",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "move_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "resolve_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "schedule_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "set_issue_security",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "transition_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "unarchive_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "view_voters_and_watchers",
"Parent": null
}
},
{
"Resource": {
"Name": "SCRUM-7",
"FullyQualifiedName": "Task/10039",
"Type": "Task",
"Metadata": {
"Project": "SCRUM",
"Status": "To Do",
"Summary": "Issue created via API"
},
"Parent": null
},
"Permission": {
"Value": "work_on_issues",
"Parent": null
}
},
{
"Resource": {
"Name": "Shahzad Haider",
"FullyQualifiedName": "User/712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Type": "User",
"Metadata": {
"AccountType": "atlassian",
"Active": "true",
"Email": "shahzadhaider@folio3.com",
"SelfURL": "https://shaider.atlassian.net/rest/api/3/user?accountId=712020:71808a62-5c16-479c-9ebd-35742afb57fa"
},
"Parent": null
},
"Permission": {
"Value": "assignable_user",
"Parent": null
}
},
{
"Resource": {
"Name": "Shahzad Haider",
"FullyQualifiedName": "User/712020:71808a62-5c16-479c-9ebd-35742afb57fa",
"Type": "User",
"Metadata": {
"AccountType": "atlassian",
"Active": "true",
"Email": "shahzadhaider@folio3.com",
"SelfURL": "https://shaider.atlassian.net/rest/api/3/user?accountId=712020:71808a62-5c16-479c-9ebd-35742afb57fa"
},
"Parent": null
},
"Permission": {
"Value": "user_picker",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": {
}
}
================================================
FILE: pkg/analyzer/analyzers/launchdarkly/launchdarkly.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go launchdarkly
package launchdarkly
import (
"errors"
"fmt"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeLaunchDarkly
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
// check if the `key` exist in the credentials info
key, exist := credInfo["key"]
if !exist {
return nil, errors.New("key not found in credentials info")
}
if isSDKKey(key) {
return nil, errors.New("sdk keys cannot be analyzed")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, token string) {
if isSDKKey(token) {
color.Yellow("\n[!] The Provided key is an SDK Key. SDK Keys are sensitive but used to configure LaunchDarkly SDKs")
color.Green("\n[i] Docs: https://launchdarkly.com/docs/home/account/environment/settings#copy-and-reset-sdk-credentials-for-an-environment")
return
}
info, err := AnalyzePermissions(cfg, token)
if err != nil {
// just print the error in cli and continue as a partial success
color.Red("[x] Error : %s", err.Error())
}
if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}
color.Green("[i] Valid LaunchDarkly Token\n")
printUser(info.User)
printPermissionsType(info.User.Token)
printResources(info.Resources)
color.Yellow("\n[!] Expires: Never")
}
// AnalyzePermissions will collect all the scopes assigned to token along with resource it can access
func AnalyzePermissions(cfg *config.Config, token string) (*SecretInfo, error) {
// create the http client
client := analyzers.NewAnalyzeClient(cfg)
var secretInfo = &SecretInfo{}
// capture user information in secretInfo
if err := CaptureUserInformation(client, token, secretInfo); err != nil {
return nil, fmt.Errorf("failed to fetch caller identity: %v", err)
}
// capture resources in secretInfo
if err := CaptureResources(client, token, secretInfo); err != nil {
return nil, fmt.Errorf("failed to fetch resources: %v", err)
}
return secretInfo, nil
}
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeLaunchDarkly,
Metadata: map[string]any{},
Bindings: make([]analyzers.Binding, 0),
}
// extract information from resource to create bindings and append to result bindings
for _, resource := range info.Resources {
binding := analyzers.Binding{
Resource: *secretInfoResourceToAnalyzerResource(resource),
Permission: analyzers.Permission{
Value: getPermissionType(info.User.Token),
},
}
if resource.ParentResource != nil {
binding.Resource.Parent = secretInfoResourceToAnalyzerResource(*resource.ParentResource)
}
result.Bindings = append(result.Bindings, binding)
}
return &result
}
// secretInfoResourceToAnalyzerResource translate secret info resource to analyzer resource for binding
func secretInfoResourceToAnalyzerResource(resource Resource) *analyzers.Resource {
analyzerRes := analyzers.Resource{
FullyQualifiedName: resource.ID,
Name: resource.Name,
Type: resource.Type,
Metadata: map[string]any{},
}
for key, value := range resource.MetaData {
analyzerRes.Metadata[key] = value
}
return &analyzerRes
}
// getPermissionType return what type of permission is assigned to token
func getPermissionType(token Token) string {
switch {
case token.Role != "":
return token.Role
case token.hasInlineRole():
return "Inline Policy"
case token.hasCustomRoles():
return "Custom Roles"
default:
return ""
}
}
// printUser print User information from secret info to cli
func printUser(user User) {
// print caller information
color.Green("\n[i] User Information:")
callerTable := table.NewWriter()
callerTable.SetOutputMirror(os.Stdout)
callerTable.AppendHeader(table.Row{"Account ID", "Member ID", "Name", "Email", "Role"})
callerTable.AppendRow(table.Row{color.GreenString(user.AccountID), color.GreenString(user.MemberID),
color.GreenString(user.Name), color.GreenString(user.Email), color.GreenString(user.Role)})
callerTable.Render()
// print token information
color.Green("\n[i] Token Information")
tokenTable := table.NewWriter()
tokenTable.SetOutputMirror(os.Stdout)
tokenTable.AppendHeader(table.Row{"ID", "Name", "Role", "Is Service Token", "Default API Version",
"No of Custom Roles Assigned", "Has Inline Policy"})
tokenTable.AppendRow(table.Row{color.GreenString(user.Token.ID), color.GreenString(user.Token.Name), color.GreenString(user.Token.Role),
color.GreenString(fmt.Sprintf("%t", user.Token.IsServiceToken)), color.GreenString(fmt.Sprintf("%d", user.Token.APIVersion)),
color.GreenString(fmt.Sprintf("%d", len(user.Token.CustomRoles))), color.GreenString(fmt.Sprintf("%t", user.Token.hasInlineRole()))})
tokenTable.Render()
// print custom roles information
if !user.Token.hasCustomRoles() {
return
}
// print token information
color.Green("\n[i] Custom Roles Assigned to Token")
rolesTable := table.NewWriter()
rolesTable.SetOutputMirror(os.Stdout)
rolesTable.AppendHeader(table.Row{"ID", "Key", "Name", "Base Permission", "Assigned to members", "Assigned to teams"})
for _, customRole := range user.Token.CustomRoles {
rolesTable.AppendRow(table.Row{color.GreenString(customRole.ID), color.GreenString(customRole.Key), color.GreenString(customRole.Name),
color.GreenString(customRole.BasePermission), color.GreenString(fmt.Sprintf("%d", customRole.AssignedToMembers)),
color.GreenString(fmt.Sprintf("%d", customRole.AssignedToTeams))})
}
rolesTable.Render()
}
// printPermissionsType print permissions type token has
func printPermissionsType(token Token) {
// print permission type. It can be either admin, writer, reader or has inline policy or any custom roles assigned
color.Green("\n[i] Permission Type: %s", getPermissionType(token))
}
func printResources(resources []Resource) {
// print resources
color.Green("\n[i] Resources:")
callerTable := table.NewWriter()
callerTable.SetOutputMirror(os.Stdout)
callerTable.AppendHeader(table.Row{"Name", "Type"})
for _, resource := range resources {
callerTable.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
}
callerTable.Render()
}
// isSDKKey check if the key provided is an SDK Key or not
func isSDKKey(key string) bool {
return strings.HasPrefix(key, "sdk-")
}
================================================
FILE: pkg/analyzer/analyzers/launchdarkly/launchdarkly_test.go
================================================
package launchdarkly
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed result_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
key := testSecrets.MustGetField("LAUNCHDARKLY_TOKEN")
tests := []struct {
name string
key string
want []byte // JSON string
wantErr bool
}{
{
name: "valid LaunchDarkly token",
key: key,
want: expectedOutput,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/launchdarkly/models.go
================================================
package launchdarkly
import "sync"
var (
MetadataKey = "key"
// resource types
applicationKey = "Application"
repositoryKey = "Repository"
projectKey = "Project"
environmentKey = "Environment"
experimentKey = "Experiment"
holdoutsKey = "Holdout"
membersKey = "Member"
destinationsKey = "Destination"
templatesKey = "Templates"
teamsKey = "Teams"
webhooksKey = "Webhooks"
featureFlagsKey = "Feature Flags"
)
type SecretInfo struct {
User User
Permissions []string
mu sync.RWMutex
Resources []Resource
}
// User is the information about the user to whom the token belongs
type User struct {
AccountID string // account id. It is the owner id of token as well
MemberID string
Name string
Role string // role of caller
Email string
Token Token
}
// Token is the token details
type Token struct {
ID string // id of the token
Name string // name of the token
CustomRoles []CustomRole // custom roles assigned to the token
InlineRole []Policy // any policy statements maybe used in place of a built-in custom role
Role string // role of token
IsServiceToken bool // is a service token or not
APIVersion int // default api version assigned to the token
}
// CustomRole is a flexible policies providing fine-grained access control to everything in launch darkly
type CustomRole struct {
ID string
Key string
Name string
Polices []Policy
BasePermission string
AssignedToMembers int
AssignedToTeams int
}
// policy is a set of statements
type Policy struct {
Resources []string
NotResources []string
Actions []string
NotActions []string
Effect string
}
type Resource struct {
ID string
Name string
Permission string
Type string
ParentResource *Resource
MetaData map[string]string
}
// appendResource append resource to secret info resources list
func (s *SecretInfo) appendResource(resource Resource) {
s.mu.Lock()
defer s.mu.Unlock()
s.Resources = append(s.Resources, resource)
}
// listResourceByType returns a list of resources matching the given type.
func (s *SecretInfo) listResourceByType(resourceType string) []Resource {
s.mu.RLock()
defer s.mu.RUnlock()
resources := make([]Resource, 0, len(s.Resources))
for _, resource := range s.Resources {
if resource.Type == resourceType {
resources = append(resources, resource)
}
}
return resources
}
// hasCustomRoles check if token has any custom roles assigned
func (t Token) hasCustomRoles() bool {
return len(t.CustomRoles) > 0
}
// hasInlineRole check if token has any inline roles
func (t Token) hasInlineRole() bool {
return len(t.InlineRole) > 0
}
================================================
FILE: pkg/analyzer/analyzers/launchdarkly/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package launchdarkly
import "errors"
type Permission int
const (
Invalid Permission = iota
Admin Permission = iota
Writer Permission = iota
Reader Permission = iota
Inlinepolicy Permission = iota
Customroles Permission = iota
)
var (
PermissionStrings = map[Permission]string{
Admin: "admin",
Writer: "writer",
Reader: "reader",
Inlinepolicy: "inlinepolicy",
Customroles: "customroles",
}
StringToPermission = map[string]Permission{
"admin": Admin,
"writer": Writer,
"reader": Reader,
"inlinepolicy": Inlinepolicy,
"customroles": Customroles,
}
PermissionIDs = map[Permission]int{
Admin: 1,
Writer: 2,
Reader: 3,
Inlinepolicy: 4,
Customroles: 5,
}
IdToPermission = map[int]Permission{
1: Admin,
2: Writer,
3: Reader,
4: Inlinepolicy,
5: Customroles,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/launchdarkly/permissions.yaml
================================================
permissions:
- admin
- writer
- reader
- inlinepolicy
- customroles
================================================
FILE: pkg/analyzer/analyzers/launchdarkly/requests.go
================================================
package launchdarkly
import (
"context"
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"sync"
"time"
)
const defaultTimeout = 5 * time.Second
var (
baseURL = "https://app.launchdarkly.com/api"
endpoints = map[string]string{
// user information APIs
"callerIdentity": "/v2/caller-identity",
"getToken": "/v2/tokens/%s", // require token id
"getRole": "/v2/roles/%s", // require role id
// resource APIs
applicationKey: "/v2/applications",
repositoryKey: "/v2/code-refs/repositories",
projectKey: "/v2/projects",
environmentKey: "/v2/projects/%s/environments", // require project key
featureFlagsKey: "/v2/flags/%s", // require project key
experimentKey: "/v2/projects/%s/environments/%s/experiments", // require project key and env key
holdoutsKey: "/v2/projects/%s/environments/%s/holdouts", // require project key and env key
membersKey: "/v2/members",
destinationsKey: "/v2/destinations",
templatesKey: "/v2/templates",
teamsKey: "/v2/teams",
webhooksKey: "/v2/webhooks",
/*
TODO:
release pipelines: https://launchdarkly.com/docs/api/release-pipelines-beta/get-all-release-pipelines (Beta)
insight deployments: https://launchdarkly.com/docs/api/insights-deployments-beta/get-deployments (Beta)
delivery configuration: https://launchdarkly.com/docs/api/integration-delivery-configurations-beta/get-integration-delivery-configuration-by-environment (Beta)
metrics: https://launchdarkly.com/docs/api/metrics-beta/get-metric-groups (Beta)
*/
}
)
// applicationsResponse is the response of /v2/applications API
type applicationsResponse struct {
Items []struct {
Key string `json:"key"`
Name string `json:"name"`
Kind string `json:"kind"`
Maintainer struct {
Email string `json:"email"`
} `json:"_maintainer"`
} `json:"items"`
}
// repositoriesResponse is the response of /v2/code-refs/repositories API
type repositoriesResponse struct {
Items []struct {
Name string `json:"name"`
Type string `json:"type"`
DefaultBranch string `json:"defaultBranch"`
SourceLink string `json:"sourceLink"`
Version int `json:"version"`
} `json:"items"`
}
// projectsResponse is the response of /v2/projects API
type projectsResponse struct {
Items []struct {
ID string `json:"_id"`
Key string `json:"key"`
Name string `json:"name"`
} `json:"items"`
}
// featureFlagsResponse is the response of /v2/flags/ API
type featureFlagsResponse struct {
Items []struct {
Key string `json:"key"`
Name string `json:"name"`
Kind string `json:"kind"`
} `json:"items"`
}
// environmentsResponse is the response of /v2/projects//environments API
type environmentsResponse struct {
Items []struct {
ID string `json:"_id"`
Key string `json:"key"`
Name string `json:"name"`
} `json:"items"`
}
// experimentResponse is the response of /v2/projects//env//experiments
type experimentResponse struct {
Items []struct {
ID string `json:"_id"`
Key string `json:"key"`
Name string `json:"name"`
MaintainerID string `json:"_maintainerId"`
} `json:"items"`
}
// membersResponse is the response of /v2/members API
type membersResponse struct {
Items []struct {
ID string `json:"_id"`
Role string `json:"role"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
} `json:"items"`
}
// holdoutsResponse is the response of /v2/projects//environments//holdouts API
type holdoutsResponse struct {
Items []struct {
ID string `json:"_id"`
Name string `json:"name"`
Key string `json:"key"`
Status string `json:"status"`
} `json:"items"`
}
// destinationsResponse is the response of /v2/destinations API
type destinationsResponse struct {
Items []struct {
ID string `json:"_id"`
Name string `json:"name"`
Kind string `json:"kind"`
Version int `json:"version"`
} `json:"items"`
}
// templatesResponse is the response of /v2/templates API
type templatesResponse struct {
Items []struct {
ID string `json:"_id"`
Key string `json:"_key"`
Name string `json:"name"`
} `json:"items"`
}
// teamsResponse is the response of /v2/teams API
type teamsResponse struct {
Items []struct {
Key string `json:"key"`
Name string `json:"name"`
Roles struct {
TotalCount int `json:"totalCount"`
} `json:"roles"`
Members struct {
TotalCount int `json:"totalCount"`
} `json:"members"`
Projects struct {
TotalCount int `json:"totalCount"`
} `json:"projects"`
} `json:"items"`
}
// webhooksResponse is the response of /v2/webhooks API
type webhooksResponse struct {
Items []struct {
ID string `json:"_id"`
Name string `json:"name"`
Url string `json:"url"`
} `json:"items"`
}
// makeLaunchDarklyRequest send the HTTP GET API request to passed url with passed token and return response body and status code
func makeLaunchDarklyRequest(client *http.Client, endpoint, token string) ([]byte, int, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
// create request
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+endpoint, http.NoBody)
if err != nil {
return nil, 0, err
}
// add required keys in the header
req.Header.Set("Authorization", token)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, 0, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
responseBodyByte, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, err
}
return responseBodyByte, resp.StatusCode, nil
}
func CaptureResources(client *http.Client, token string, secretInfo *SecretInfo) error {
var (
wg sync.WaitGroup
errAggWg sync.WaitGroup
aggregatedErrs = make([]error, 0)
errChan = make(chan error, 1)
)
errAggWg.Add(1)
go func() {
defer errAggWg.Done()
for err := range errChan {
aggregatedErrs = append(aggregatedErrs, err)
}
}()
// helper to launch tasks concurrently.
launchTask := func(task func() error) {
wg.Add(1)
go func() {
defer wg.Done()
if err := task(); err != nil {
errChan <- err
}
}()
}
// capture top-level resources
launchTask(func() error { return captureApplications(client, token, secretInfo) })
launchTask(func() error { return captureRepositories(client, token, secretInfo) })
// capture projects
launchTask(func() error {
if err := captureProjects(client, token, secretInfo); err != nil {
return err
}
// capture project sub resources
projects := secretInfo.listResourceByType(projectKey)
for _, proj := range projects {
launchTask(func() error { return captureProjectFeatureFlags(client, token, proj, secretInfo) })
launchTask(func() error { return captureProjectEnv(client, token, proj, secretInfo) })
}
return nil
})
launchTask(func() error { return captureMembers(client, token, secretInfo) })
launchTask(func() error { return captureDestinations(client, token, secretInfo) })
launchTask(func() error { return captureTemplates(client, token, secretInfo) })
launchTask(func() error { return captureTeams(client, token, secretInfo) })
launchTask(func() error { return captureWebhooks(client, token, secretInfo) })
wg.Wait()
close(errChan)
errAggWg.Wait()
if len(aggregatedErrs) > 0 {
return errors.Join(aggregatedErrs...)
}
return nil
}
// docs: https://launchdarkly.com/docs/api/applications-beta/get-applications
func captureApplications(client *http.Client, token string, secretInfo *SecretInfo) error {
response, statusCode, err := makeLaunchDarklyRequest(client, endpoints[applicationKey], token)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var applications = applicationsResponse{}
if err := json.Unmarshal(response, &applications); err != nil {
return err
}
for _, application := range applications.Items {
resource := Resource{
ID: fmt.Sprintf("launchdarkly/app/%s", application.Key),
Name: application.Name,
Type: applicationKey,
MetaData: map[string]string{
"Maintainer Email": application.Maintainer.Email,
"Kind": application.Kind,
MetadataKey: application.Key,
},
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// docs: https://launchdarkly.com/docs/api/code-references/get-repositories
func captureRepositories(client *http.Client, token string, secretInfo *SecretInfo) error {
response, statusCode, err := makeLaunchDarklyRequest(client, endpoints[repositoryKey], token)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var repositories = repositoriesResponse{}
if err := json.Unmarshal(response, &repositories); err != nil {
return err
}
for _, repository := range repositories.Items {
resource := Resource{
ID: fmt.Sprintf("%s/repo/%s/%d", repository.Type, repository.Name, repository.Version), // no unique id exist, so we make one
Name: repository.Name,
Type: repositoryKey,
MetaData: map[string]string{
"Default branch": repository.DefaultBranch,
"Version": strconv.Itoa(repository.Version),
"Source link": repository.SourceLink,
MetadataKey: repositoryKey,
},
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// docs: https://launchdarkly.com/docs/api/projects/get-projects
func captureProjects(client *http.Client, token string, secretInfo *SecretInfo) error {
response, statusCode, err := makeLaunchDarklyRequest(client, endpoints[projectKey], token)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var projects = projectsResponse{}
if err := json.Unmarshal(response, &projects); err != nil {
return err
}
for _, project := range projects.Items {
secretInfo.appendResource(Resource{
ID: fmt.Sprintf("launchdarkly/proj/%s", project.ID),
Name: project.Name,
Type: projectKey,
MetaData: map[string]string{
MetadataKey: project.Key,
},
})
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// docs: https://launchdarkly.com/docs/api/feature-flags/get-feature-flags
func captureProjectFeatureFlags(client *http.Client, token string, parent Resource, secretInfo *SecretInfo) error {
projectKey, exist := parent.MetaData[MetadataKey]
if !exist {
return errors.New("project key not found")
}
response, statusCode, err := makeLaunchDarklyRequest(client, fmt.Sprintf(endpoints[featureFlagsKey], projectKey), token)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var flags = featureFlagsResponse{}
if err := json.Unmarshal(response, &flags); err != nil {
return err
}
for _, flag := range flags.Items {
resource := Resource{
ID: fmt.Sprintf("launchdarkly/proj/%s/flag/%s", projectKey, flag.Key),
Name: flag.Name,
Type: featureFlagsKey,
MetaData: map[string]string{
"Kind": flag.Kind,
},
ParentResource: &parent,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// docs: https://launchdarkly.com/docs/api/environments/get-environments-by-project
func captureProjectEnv(client *http.Client, token string, parent Resource, secretInfo *SecretInfo) error {
projectKey, exist := parent.MetaData[MetadataKey]
if !exist {
return errors.New("project key not found")
}
response, statusCode, err := makeLaunchDarklyRequest(client, fmt.Sprintf(endpoints[environmentKey], projectKey), token)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var envs = environmentsResponse{}
if err := json.Unmarshal(response, &envs); err != nil {
return err
}
for _, env := range envs.Items {
resource := Resource{
ID: fmt.Sprintf("launchdarkly/%s/env/%s", projectKey, env.ID),
Name: env.Name,
Type: environmentKey,
MetaData: map[string]string{
MetadataKey: env.Key,
},
ParentResource: &parent,
}
secretInfo.appendResource(resource)
// capture project env child resources
if err := captureProjectEnvExperiments(client, token, projectKey, resource, secretInfo); err != nil {
return err
}
if err := captureProjectHoldouts(client, token, projectKey, resource, secretInfo); err != nil {
return err
}
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// docs: https://launchdarkly.com/docs/api/experiments/get-experiments
func captureProjectEnvExperiments(client *http.Client, token string, projectKey string, parent Resource, secretInfo *SecretInfo) error {
envKey, exist := parent.MetaData[MetadataKey]
if !exist {
return errors.New("env key not found")
}
response, statusCode, err := makeLaunchDarklyRequest(client, fmt.Sprintf(endpoints[experimentKey], projectKey, envKey), token)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var exps = experimentResponse{}
if err := json.Unmarshal(response, &exps); err != nil {
return err
}
for _, exp := range exps.Items {
resource := Resource{
ID: fmt.Sprintf("launchdarkly/%s/env/%s/exp/%s", projectKey, envKey, exp.ID),
Name: exp.Name,
Type: experimentKey,
MetaData: map[string]string{
MetadataKey: exp.Key,
"Maintiner ID": exp.MaintainerID,
},
ParentResource: &parent,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound:
return nil
case http.StatusTooManyRequests:
time.Sleep(1 * time.Second)
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// docs: https://launchdarkly.com/docs/api/holdouts-beta/get-all-holdouts
func captureProjectHoldouts(client *http.Client, token string, projectKey string, parent Resource, secretInfo *SecretInfo) error {
envKey, exist := parent.MetaData[MetadataKey]
if !exist {
return errors.New("env key not found")
}
response, statusCode, err := makeLaunchDarklyRequest(client, fmt.Sprintf(endpoints[holdoutsKey], projectKey, envKey), token)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var holdouts = holdoutsResponse{}
if err := json.Unmarshal(response, &holdouts); err != nil {
return err
}
for _, holdout := range holdouts.Items {
resource := Resource{
ID: fmt.Sprintf("launchdarkly/%s/env/%s/holdout/%s", projectKey, envKey, holdout.ID),
Name: holdout.Name,
Type: holdoutsKey,
MetaData: map[string]string{
"Status": holdout.Status,
holdoutsKey: holdout.Key,
},
ParentResource: &parent,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// docs: https://launchdarkly.com/docs/api/account-members/get-members
func captureMembers(client *http.Client, token string, secretInfo *SecretInfo) error {
response, statusCode, err := makeLaunchDarklyRequest(client, endpoints[membersKey], token)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var members = membersResponse{}
if err := json.Unmarshal(response, &members); err != nil {
return err
}
for _, member := range members.Items {
resource := Resource{
ID: fmt.Sprintf("launchdarkly/member/%s", member.ID),
Name: member.FirstName + " " + member.LastName,
Type: membersKey,
MetaData: map[string]string{
"Role": member.Role,
"Email": member.Email,
},
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// docs: https://launchdarkly.com/docs/api/data-export-destinations/get-destinations
func captureDestinations(client *http.Client, token string, secretInfo *SecretInfo) error {
response, statusCode, err := makeLaunchDarklyRequest(client, endpoints[destinationsKey], token)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var destinations = destinationsResponse{}
if err := json.Unmarshal(response, &destinations); err != nil {
return err
}
for _, destination := range destinations.Items {
resource := Resource{
ID: fmt.Sprintf("launchdarkly/destination/%s", destination.ID),
Name: destination.Name,
Type: destinationsKey,
MetaData: map[string]string{
"Kind": destination.Kind,
"Version": strconv.Itoa(destination.Version),
},
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// docs: https://launchdarkly.com/docs/api/workflow-templates/get-workflow-templates
func captureTemplates(client *http.Client, token string, secretInfo *SecretInfo) error {
response, statusCode, err := makeLaunchDarklyRequest(client, endpoints[templatesKey], token)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var templates = templatesResponse{}
if err := json.Unmarshal(response, &templates); err != nil {
return err
}
for _, template := range templates.Items {
resource := Resource{
ID: fmt.Sprintf("launchdarkly/templates/%s", template.ID),
Name: template.Name,
Type: templatesKey,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// docs: https://launchdarkly.com/docs/api/teams/get-teams
func captureTeams(client *http.Client, token string, secretInfo *SecretInfo) error {
response, statusCode, err := makeLaunchDarklyRequest(client, endpoints[teamsKey], token)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var teams = teamsResponse{}
if err := json.Unmarshal(response, &teams); err != nil {
return err
}
for _, team := range teams.Items {
resource := Resource{
ID: fmt.Sprintf("launchdarkly/teams/%s", team.Key),
Name: team.Name,
Type: teamsKey,
MetaData: map[string]string{
"Total Roles Count": strconv.Itoa(team.Roles.TotalCount),
"Total Memvers Count": strconv.Itoa(team.Members.TotalCount),
"Total Projects Count": strconv.Itoa(team.Projects.TotalCount),
},
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// docs: https://launchdarkly.com/docs/api/webhooks/get-all-webhooks
func captureWebhooks(client *http.Client, token string, secretInfo *SecretInfo) error {
response, statusCode, err := makeLaunchDarklyRequest(client, endpoints[webhooksKey], token)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var webhooks = webhooksResponse{}
if err := json.Unmarshal(response, &webhooks); err != nil {
return err
}
for _, webhook := range webhooks.Items {
resource := Resource{
ID: fmt.Sprintf("launchdarkly/webhooks/%s", webhook.ID),
Name: webhook.Name,
Type: webhooksKey,
}
secretInfo.appendResource(resource)
}
return nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
================================================
FILE: pkg/analyzer/analyzers/launchdarkly/result_output.json
================================================
{
"AnalyzerType": 31,
"Bindings": [
{
"Resource": {
"Name": "Production",
"FullyQualifiedName": "launchdarkly/default/env/61543c5956be602355624871",
"Type": "Environment",
"Metadata": {
"key": "production"
},
"Parent": {
"Name": "secretscanner",
"FullyQualifiedName": "launchdarkly/proj/61543c5956be60235562486e",
"Type": "Project",
"Metadata": {
"key": "default"
},
"Parent": null
}
},
"Permission": {
"Value": "admin",
"Parent": null
}
},
{
"Resource": {
"Name": "Roxanne Tampus",
"FullyQualifiedName": "launchdarkly/member/61543c5956be60235562486f",
"Type": "Member",
"Metadata": {
"Email": "knightmoverchan@gmail.com",
"Role": "owner"
},
"Parent": null
},
"Permission": {
"Value": "admin",
"Parent": null
}
},
{
"Resource": {
"Name": "Test",
"FullyQualifiedName": "launchdarkly/default/env/61543c5956be602355624870",
"Type": "Environment",
"Metadata": {
"key": "test"
},
"Parent": {
"Name": "secretscanner",
"FullyQualifiedName": "launchdarkly/proj/61543c5956be60235562486e",
"Type": "Project",
"Metadata": {
"key": "default"
},
"Parent": null
}
},
"Permission": {
"Value": "admin",
"Parent": null
}
},
{
"Resource": {
"Name": "secretscanner",
"FullyQualifiedName": "launchdarkly/proj/61543c5956be60235562486e",
"Type": "Project",
"Metadata": {
"key": "default"
},
"Parent": null
},
"Permission": {
"Value": "admin",
"Parent": null
}
},
{
"Resource": {
"Name": "secretscanner",
"FullyQualifiedName": "launchdarkly/proj/default/flag/secretscanner",
"Type": "Feature Flags",
"Metadata": {
"Kind": "boolean"
},
"Parent": {
"Name": "secretscanner",
"FullyQualifiedName": "launchdarkly/proj/61543c5956be60235562486e",
"Type": "Project",
"Metadata": {
"key": "default"
},
"Parent": null
}
},
"Permission": {
"Value": "admin",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": {}
}
================================================
FILE: pkg/analyzer/analyzers/launchdarkly/user.go
================================================
/*
user.go file is all related to calling APIs to get user and token information and formatting them to secretInfo User.
It calls 3 APIs:
- /v2/caller-identity
- /v2/tokens/ (with token id from previous api response)
- /v2/roles/ (if custom role id is present in tokens) (more than one role can be assigned to token as well)
it formats all these responses into one User struct for secretInfo.
*/
package launchdarkly
import (
"encoding/json"
"errors"
"fmt"
"net/http"
)
// callerIdentityResponse is /v2/caller-identity API response
type callerIdentityResponse struct {
AccountID string `json:"accountId"`
TokenName string `json:"tokenName"`
TokenID string `json:"tokenId"`
MemberID string `json:"memberId"`
ServiceToken bool `json:"serviceToken"`
}
// tokenResponse is the /v2/tokens/ API response
type tokenResponse struct {
OwnerID string `json:"ownerId"`
Member tokenMemberResponse `json:"_member"`
Name string `json:"name"`
CustomRoleIDs []string `json:"customRoleIds,omitempty"`
InlineRole []tokenPolicyResponse `json:"inlineRole,omitempty"`
Role string `json:"role"`
ServiceToken bool `json:"serviceToken"`
DefaultAPIVersion int `json:"defaultApiVersion"`
}
// _member object in token response
type tokenMemberResponse struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Role string `json:"role"`
Email string `json:"email"`
}
// inlineRole object in token response
type tokenPolicyResponse struct {
Effect string `json:"effect,omitempty"`
Resources []string `json:"resources,omitempty"`
NotResources []string `json:"notResources,omitempty"`
Actions []string `json:"actions,omitempty"`
NotActions []string `json:"notActions,omitempty"`
}
// customRoleResponse is the /v2/roles/ API response
type customRoleResponse struct {
ID string `json:"_id"`
Key string `json:"key"`
Name string `json:"name"`
Policy []tokenPolicyResponse `json:"policy"`
BasePermission string `json:"basePermissions"`
AssignedTo struct {
MembersCount int `json:"membersCount"`
TeamsCount int `json:"teamsCount"`
} `json:"assignedTo"`
}
/*
CaptureUserInformation call following three APIs:
- /v2/caller-identity
- /v2/tokens/ (token_id from previous API response)
- /v2/roles/ (roles_id from previous API response if exist)
It format all responses into one secret info User
*/
func CaptureUserInformation(client *http.Client, token string, secretInfo *SecretInfo) error {
caller, err := getCallerIdentity(client, token)
if err != nil {
return err
}
tokenDetails, err := getToken(client, caller.TokenID, token)
if err != nil {
return err
}
customRoles, err := getCustomRole(client, tokenDetails.CustomRoleIDs, token)
if err != nil {
return err
}
addUserToSecretInfo(caller, tokenDetails, customRoles, secretInfo)
return nil
}
// getCallerIdentity call /v2/caller-identity API and return response
func getCallerIdentity(client *http.Client, token string) (*callerIdentityResponse, error) {
response, statusCode, err := makeLaunchDarklyRequest(client, endpoints["callerIdentity"], token)
if err != nil {
return nil, err
}
switch statusCode {
case http.StatusOK:
var caller = &callerIdentityResponse{}
if err := json.Unmarshal(response, caller); err != nil {
return caller, err
}
return caller, nil
case http.StatusUnauthorized:
return nil, errors.New("invalid token; failed to get caller information")
default:
return nil, fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// getToken call /v2/tokens/ API and return response
func getToken(client *http.Client, tokenID, token string) (*tokenResponse, error) {
response, statusCode, err := makeLaunchDarklyRequest(client, fmt.Sprintf(endpoints["getToken"], tokenID), token)
if err != nil {
return nil, err
}
switch statusCode {
case http.StatusOK:
var token tokenResponse
if err := json.Unmarshal(response, &token); err != nil {
return nil, err
}
return &token, nil
case http.StatusUnauthorized:
return nil, errors.New("invalid token; failed to get token information")
default:
return nil, fmt.Errorf("unexpected status code: %d", statusCode)
}
}
// getCustomRole call /v2/roles/ API for all IDs passed and return list of responses
func getCustomRole(client *http.Client, customRoleIDs []string, token string) ([]customRoleResponse, error) {
var customRoles []customRoleResponse
for _, customRoleID := range customRoleIDs {
response, statusCode, err := makeLaunchDarklyRequest(client, fmt.Sprintf(endpoints["getRole"], customRoleID), token)
if err != nil {
return nil, err
}
switch statusCode {
case http.StatusOK:
var customRole customRoleResponse
if err := json.Unmarshal(response, &customRole); err != nil {
return nil, err
}
customRoles = append(customRoles, customRole)
case http.StatusUnauthorized:
return nil, nil
default:
return nil, fmt.Errorf("unexpected status code: %d", statusCode)
}
}
return customRoles, nil
}
// makeCallerIdentity take caller, tokenDetails, and customRoles and return secret info CallerIdentity
func addUserToSecretInfo(caller *callerIdentityResponse, tokenDetails *tokenResponse, customRoles []customRoleResponse, secretInfo *SecretInfo) {
user := User{
AccountID: caller.AccountID,
MemberID: caller.MemberID,
Name: tokenDetails.Member.FirstName + " " + tokenDetails.Member.LastName,
Role: tokenDetails.Member.Role,
Email: tokenDetails.Member.Email,
Token: Token{
ID: caller.TokenID,
Name: tokenDetails.Name,
Role: tokenDetails.Role,
APIVersion: tokenDetails.DefaultAPIVersion,
IsServiceToken: tokenDetails.ServiceToken,
InlineRole: toPolicy(tokenDetails.InlineRole),
CustomRoles: toCustomRoles(customRoles),
},
}
secretInfo.User = user
}
// toPolicy convert inlinePolicy from token response to secret info caller identity policy
func toPolicy(inlinePolices []tokenPolicyResponse) []Policy {
var policies = make([]Policy, 0)
for _, inlinePolicy := range inlinePolices {
policies = append(policies, Policy{
Resources: inlinePolicy.Resources,
NotResources: inlinePolicy.NotResources,
Actions: inlinePolicy.Actions,
NotActions: inlinePolicy.NotActions,
Effect: inlinePolicy.Effect,
})
}
return policies
}
// toCustomRoles convert customRole from token response to secret info caller identity custom role
func toCustomRoles(roles []customRoleResponse) []CustomRole {
var customRoles = make([]CustomRole, 0)
for _, role := range roles {
customRoles = append(customRoles, CustomRole{
ID: role.ID,
Key: role.Key,
Name: role.Name,
Polices: toPolicy(role.Policy),
BasePermission: role.BasePermission,
AssignedToMembers: role.AssignedTo.MembersCount,
AssignedToTeams: role.AssignedTo.TeamsCount,
})
}
return customRoles
}
================================================
FILE: pkg/analyzer/analyzers/mailchimp/expected_output.json
================================================
{"AnalyzerType":7,"Bindings":[{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"account_export","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"add_contacts","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"add_files_to_content_studio","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"add_or_access_api_keys","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"archive_contacts","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"audience_export","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"audience_import","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"change_billing_information","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"change_company_organization_name","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"check_reconnect_integrations","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"close_account","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"connect_a_domain","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"create_a_landing_page","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"create_audiences","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"create_customer_journey","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"create_emails","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"create_form","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"create_or_import_templates","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"create_send_sms_mms_messages","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"create_survey","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"create_your_website","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"delete_contacts","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"delete_emails","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"delete_form","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"delete_survey","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"domain_performance","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"e_commerce_product_activity","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"edit_audience_settings","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"edit_customer_journey","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"edit_emails","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"edit_form","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"edit_survey","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"edit_templates","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"email_contact_details","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"email_open_details","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"invite_users","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"leave_comments","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"opt_in_to_receive_emails_from_mailchimp","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"pause_unpublish_emails","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"publish_form","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"publish_survey","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"publish_unpublish_a_landing_page","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"publish_unpublish_your_website","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"purchase_sms_credits","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"referral_program","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"replicate_a_landing_page","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"require_2_factor_authentication","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"revoke_account_access","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"send_messages","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"send_publish_emails","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"set_user_access_level","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"submit_sms_marketing_application","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"toggle_user_notifications","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"top_locations","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"turn_on_pause_turn_back_on","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"use_conversations","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"verify_a_domain","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"view_abuse_reports","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"view_audiences","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"view_customer_journey","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"view_email_recipients","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"view_email_reports","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"view_email_statistics","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"view_messages","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"view_report","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"view_segments","Parent":null}},{"Resource":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null},"Permission":{"Value":"view_sms_reports","Parent":null}}],"UnboundedResources":[{"Name":"trufflesec.com","FullyQualifiedName":"mailchimp.com/domain/trufflesec.com","Type":"domain","Metadata":{"authenticated":false,"verified":true},"Parent":{"Name":"TruffleHog","FullyQualifiedName":"mailchimp.com/account/09f02f6ec9b78ff5c3ce52f96","Type":"account","Metadata":{"account_timezone":"America/New_York","email":"detectors@trufflesec.com","last_login":"2024-08-16T10:39:15+00:00","member_since":"2024-08-16T10:37:37+00:00","pricing_plan":"forever_free","role":"owner","total_subscribers":1},"Parent":null}}],"Metadata":null}
================================================
FILE: pkg/analyzer/analyzers/mailchimp/mailchimp.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go mailchimp
package mailchimp
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
const BASE_URL = "https://%s.api.mailchimp.com/3.0"
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeMailchimp }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, errors.New("key not found in credentialInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeMailchimp,
Bindings: make([]analyzers.Binding, 0, len(StringToPermission)),
UnboundedResources: make([]analyzers.Resource, 0, len(info.Domains.Domains)),
}
accountResource := analyzers.Resource{
Name: info.Metadata.AccountName,
FullyQualifiedName: "mailchimp.com/account/" + info.Metadata.AccountID,
Type: "account",
Metadata: map[string]any{
"email": info.Metadata.Email,
"role": info.Metadata.Role,
"member_since": info.Metadata.MemberSince,
"pricing_plan": info.Metadata.PricingPlan,
"account_timezone": info.Metadata.AccountTimezone,
"last_login": info.Metadata.LastLogin,
"total_subscribers": info.Metadata.TotalSubscribers,
},
}
for perm := range StringToPermission {
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: accountResource,
Permission: analyzers.Permission{
Value: perm,
},
})
}
for _, domain := range info.Domains.Domains {
result.UnboundedResources = append(result.UnboundedResources, analyzers.Resource{
Name: domain.Domain,
FullyQualifiedName: "mailchimp.com/domain/" + domain.Domain,
Type: "domain",
Metadata: map[string]any{
"verified": domain.Verified,
"authenticated": domain.Authenticated,
},
Parent: &accountResource,
})
}
return &result
}
type MetadataJSON struct {
AccountID string `json:"account_id"`
AccountName string `json:"account_name"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Role string `json:"role"`
MemberSince string `json:"member_since"`
PricingPlan string `json:"pricing_plan_type"`
AccountTimezone string `json:"account_timezone"`
Contact struct {
Company string `json:"company"`
Address1 string `json:"addr1"`
Address2 string `json:"addr2"`
City string `json:"city"`
State string `json:"state"`
Zip string `json:"zip"`
Country string `json:"country"`
} `json:"contact"`
LastLogin string `json:"last_login"`
TotalSubscribers int `json:"total_subscribers"`
}
type DomainsJSON struct {
Domains []Domain `json:"domains"`
}
type Domain struct {
Domain string `json:"domain"`
Authenticated bool `json:"authenticated"`
Verified bool `json:"verified"`
}
func getMetadata(cfg *config.Config, key string) (MetadataJSON, error) {
var metadata MetadataJSON
// extract datacenter
keySplit := strings.Split(key, "-")
if len(keySplit) != 2 {
return metadata, nil
}
datacenter := keySplit[1]
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", fmt.Sprintf(BASE_URL, datacenter), nil)
if err != nil {
return metadata, err
}
req.SetBasicAuth("anystring", key)
resp, err := client.Do(req)
if err != nil {
return metadata, err
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&metadata); err != nil {
return metadata, err
}
return metadata, nil
}
func getDomains(cfg *config.Config, key string) (DomainsJSON, error) {
var domains DomainsJSON
// extract datacenter
keySplit := strings.Split(key, "-")
if len(keySplit) != 2 {
return domains, nil
}
datacenter := keySplit[1]
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", fmt.Sprintf(BASE_URL, datacenter)+"/verified-domains", nil)
if err != nil {
return domains, err
}
req.SetBasicAuth("anystring", key)
resp, err := client.Do(req)
if err != nil {
return domains, err
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&domains); err != nil {
return domains, err
}
return domains, nil
}
type SecretInfo struct {
Metadata MetadataJSON
Domains DomainsJSON
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
// get metadata
metadata, err := getMetadata(cfg, key)
if err != nil {
return nil, err
}
if metadata.AccountID == "" {
return nil, fmt.Errorf("Invalid Mailchimp API key")
}
// get sending domains
domains, err := getDomains(cfg, key)
if err != nil {
return nil, err
}
return &SecretInfo{
Metadata: metadata,
Domains: domains,
}, nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
printMetadata(info.Metadata)
// print full api key permissions
color.Green("\n[i] Permissions: Full Access\n\n")
// print sending domains
if len(info.Domains.Domains) > 0 {
printDomains(info.Domains)
} else {
color.Yellow("[i] No sending domains found\n")
}
}
func printMetadata(metadata MetadataJSON) {
color.Green("[!] Valid Mailchimp API key\n\n")
// print table with account info
color.Yellow("[i] Mailchimp Account Info:\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendRow([]any{("Account Name"), color.GreenString("%s", metadata.AccountName)})
t.AppendRow([]any{("Company Name"), color.GreenString("%s", metadata.Contact.Company)})
t.AppendRow([]any{("Address"), color.GreenString("%s %s\n%s, %s %s\n%s", metadata.Contact.Address1, metadata.Contact.Address2, metadata.Contact.City, metadata.Contact.State, metadata.Contact.Zip, metadata.Contact.Country)})
t.AppendRow([]any{("Total Subscribers"), color.GreenString("%d", metadata.TotalSubscribers)})
t.Render()
// print user info
color.Yellow("\n[i] Mailchimp User Info:\n")
t = table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendRow([]any{("User Name"), color.GreenString("%s %s", metadata.FirstName, metadata.LastName)})
t.AppendRow([]any{("User Email"), color.GreenString("%s", metadata.Email)})
t.AppendRow([]any{("User Role"), color.GreenString("%s", metadata.Role)})
t.AppendRow([]any{("Last Login"), color.GreenString("%s", metadata.LastLogin)})
t.AppendRow([]any{("Member Since"), color.GreenString("%s", metadata.MemberSince)})
t.Render()
}
func printDomains(domains DomainsJSON) {
color.Yellow("\n[i] Sending Domains:\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Domain", "Enabled and Verified"})
for _, domain := range domains.Domains {
authenticated := ""
if domain.Authenticated && domain.Verified {
authenticated = color.GreenString("Yes")
} else {
authenticated = color.RedString("No")
}
t.AppendRow([]any{color.GreenString(domain.Domain), authenticated})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/mailchimp/mailchimp_test.go
================================================
package mailchimp
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expected_output []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid Mailchimp key",
key: testSecrets.MustGetField("MAILCHIMP"),
want: string(expected_output),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.Marshal(got)
// gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/mailchimp/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package mailchimp
import "errors"
type Permission int
const (
Invalid Permission = iota
InviteUsers Permission = iota
RevokeAccountAccess Permission = iota
SetUserAccessLevel Permission = iota
Require2FactorAuthentication Permission = iota
ChangeBillingInformation Permission = iota
ChangeCompanyOrganizationName Permission = iota
AddOrAccessApiKeys Permission = iota
CheckReconnectIntegrations Permission = iota
ReferralProgram Permission = iota
AccountExport Permission = iota
CloseAccount Permission = iota
AddFilesToContentStudio Permission = iota
OptInToReceiveEmailsFromMailchimp Permission = iota
CreateAudiences Permission = iota
ViewAudiences Permission = iota
AudienceExport Permission = iota
AudienceImport Permission = iota
AddContacts Permission = iota
DeleteContacts Permission = iota
ViewSegments Permission = iota
EditAudienceSettings Permission = iota
ArchiveContacts Permission = iota
CreateOrImportTemplates Permission = iota
EditTemplates Permission = iota
CreateEmails Permission = iota
EditEmails Permission = iota
SendPublishEmails Permission = iota
PauseUnpublishEmails Permission = iota
DeleteEmails Permission = iota
SubmitSmsMarketingApplication Permission = iota
CreateSendSmsMmsMessages Permission = iota
PurchaseSmsCredits Permission = iota
ViewEmailReports Permission = iota
ViewSmsReports Permission = iota
ViewAbuseReports Permission = iota
ViewEmailStatistics Permission = iota
UseConversations Permission = iota
ViewEmailRecipients Permission = iota
TopLocations Permission = iota
EmailContactDetails Permission = iota
EmailOpenDetails Permission = iota
ECommerceProductActivity Permission = iota
DomainPerformance Permission = iota
CreateYourWebsite Permission = iota
PublishUnpublishYourWebsite Permission = iota
ViewReport Permission = iota
CreateALandingPage Permission = iota
PublishUnpublishALandingPage Permission = iota
ReplicateALandingPage Permission = iota
VerifyADomain Permission = iota
ConnectADomain Permission = iota
CreateCustomerJourney Permission = iota
ViewCustomerJourney Permission = iota
EditCustomerJourney Permission = iota
TurnOnPauseTurnBackOn Permission = iota
ViewMessages Permission = iota
LeaveComments Permission = iota
SendMessages Permission = iota
ToggleUserNotifications Permission = iota
CreateSurvey Permission = iota
EditSurvey Permission = iota
PublishSurvey Permission = iota
DeleteSurvey Permission = iota
CreateForm Permission = iota
EditForm Permission = iota
PublishForm Permission = iota
DeleteForm Permission = iota
)
var (
PermissionStrings = map[Permission]string{
InviteUsers: "invite_users",
RevokeAccountAccess: "revoke_account_access",
SetUserAccessLevel: "set_user_access_level",
Require2FactorAuthentication: "require_2_factor_authentication",
ChangeBillingInformation: "change_billing_information",
ChangeCompanyOrganizationName: "change_company_organization_name",
AddOrAccessApiKeys: "add_or_access_api_keys",
CheckReconnectIntegrations: "check_reconnect_integrations",
ReferralProgram: "referral_program",
AccountExport: "account_export",
CloseAccount: "close_account",
AddFilesToContentStudio: "add_files_to_content_studio",
OptInToReceiveEmailsFromMailchimp: "opt_in_to_receive_emails_from_mailchimp",
CreateAudiences: "create_audiences",
ViewAudiences: "view_audiences",
AudienceExport: "audience_export",
AudienceImport: "audience_import",
AddContacts: "add_contacts",
DeleteContacts: "delete_contacts",
ViewSegments: "view_segments",
EditAudienceSettings: "edit_audience_settings",
ArchiveContacts: "archive_contacts",
CreateOrImportTemplates: "create_or_import_templates",
EditTemplates: "edit_templates",
CreateEmails: "create_emails",
EditEmails: "edit_emails",
SendPublishEmails: "send_publish_emails",
PauseUnpublishEmails: "pause_unpublish_emails",
DeleteEmails: "delete_emails",
SubmitSmsMarketingApplication: "submit_sms_marketing_application",
CreateSendSmsMmsMessages: "create_send_sms_mms_messages",
PurchaseSmsCredits: "purchase_sms_credits",
ViewEmailReports: "view_email_reports",
ViewSmsReports: "view_sms_reports",
ViewAbuseReports: "view_abuse_reports",
ViewEmailStatistics: "view_email_statistics",
UseConversations: "use_conversations",
ViewEmailRecipients: "view_email_recipients",
TopLocations: "top_locations",
EmailContactDetails: "email_contact_details",
EmailOpenDetails: "email_open_details",
ECommerceProductActivity: "e_commerce_product_activity",
DomainPerformance: "domain_performance",
CreateYourWebsite: "create_your_website",
PublishUnpublishYourWebsite: "publish_unpublish_your_website",
ViewReport: "view_report",
CreateALandingPage: "create_a_landing_page",
PublishUnpublishALandingPage: "publish_unpublish_a_landing_page",
ReplicateALandingPage: "replicate_a_landing_page",
VerifyADomain: "verify_a_domain",
ConnectADomain: "connect_a_domain",
CreateCustomerJourney: "create_customer_journey",
ViewCustomerJourney: "view_customer_journey",
EditCustomerJourney: "edit_customer_journey",
TurnOnPauseTurnBackOn: "turn_on_pause_turn_back_on",
ViewMessages: "view_messages",
LeaveComments: "leave_comments",
SendMessages: "send_messages",
ToggleUserNotifications: "toggle_user_notifications",
CreateSurvey: "create_survey",
EditSurvey: "edit_survey",
PublishSurvey: "publish_survey",
DeleteSurvey: "delete_survey",
CreateForm: "create_form",
EditForm: "edit_form",
PublishForm: "publish_form",
DeleteForm: "delete_form",
}
StringToPermission = map[string]Permission{
"invite_users": InviteUsers,
"revoke_account_access": RevokeAccountAccess,
"set_user_access_level": SetUserAccessLevel,
"require_2_factor_authentication": Require2FactorAuthentication,
"change_billing_information": ChangeBillingInformation,
"change_company_organization_name": ChangeCompanyOrganizationName,
"add_or_access_api_keys": AddOrAccessApiKeys,
"check_reconnect_integrations": CheckReconnectIntegrations,
"referral_program": ReferralProgram,
"account_export": AccountExport,
"close_account": CloseAccount,
"add_files_to_content_studio": AddFilesToContentStudio,
"opt_in_to_receive_emails_from_mailchimp": OptInToReceiveEmailsFromMailchimp,
"create_audiences": CreateAudiences,
"view_audiences": ViewAudiences,
"audience_export": AudienceExport,
"audience_import": AudienceImport,
"add_contacts": AddContacts,
"delete_contacts": DeleteContacts,
"view_segments": ViewSegments,
"edit_audience_settings": EditAudienceSettings,
"archive_contacts": ArchiveContacts,
"create_or_import_templates": CreateOrImportTemplates,
"edit_templates": EditTemplates,
"create_emails": CreateEmails,
"edit_emails": EditEmails,
"send_publish_emails": SendPublishEmails,
"pause_unpublish_emails": PauseUnpublishEmails,
"delete_emails": DeleteEmails,
"submit_sms_marketing_application": SubmitSmsMarketingApplication,
"create_send_sms_mms_messages": CreateSendSmsMmsMessages,
"purchase_sms_credits": PurchaseSmsCredits,
"view_email_reports": ViewEmailReports,
"view_sms_reports": ViewSmsReports,
"view_abuse_reports": ViewAbuseReports,
"view_email_statistics": ViewEmailStatistics,
"use_conversations": UseConversations,
"view_email_recipients": ViewEmailRecipients,
"top_locations": TopLocations,
"email_contact_details": EmailContactDetails,
"email_open_details": EmailOpenDetails,
"e_commerce_product_activity": ECommerceProductActivity,
"domain_performance": DomainPerformance,
"create_your_website": CreateYourWebsite,
"publish_unpublish_your_website": PublishUnpublishYourWebsite,
"view_report": ViewReport,
"create_a_landing_page": CreateALandingPage,
"publish_unpublish_a_landing_page": PublishUnpublishALandingPage,
"replicate_a_landing_page": ReplicateALandingPage,
"verify_a_domain": VerifyADomain,
"connect_a_domain": ConnectADomain,
"create_customer_journey": CreateCustomerJourney,
"view_customer_journey": ViewCustomerJourney,
"edit_customer_journey": EditCustomerJourney,
"turn_on_pause_turn_back_on": TurnOnPauseTurnBackOn,
"view_messages": ViewMessages,
"leave_comments": LeaveComments,
"send_messages": SendMessages,
"toggle_user_notifications": ToggleUserNotifications,
"create_survey": CreateSurvey,
"edit_survey": EditSurvey,
"publish_survey": PublishSurvey,
"delete_survey": DeleteSurvey,
"create_form": CreateForm,
"edit_form": EditForm,
"publish_form": PublishForm,
"delete_form": DeleteForm,
}
PermissionIDs = map[Permission]int{
InviteUsers: 1,
RevokeAccountAccess: 2,
SetUserAccessLevel: 3,
Require2FactorAuthentication: 4,
ChangeBillingInformation: 5,
ChangeCompanyOrganizationName: 6,
AddOrAccessApiKeys: 7,
CheckReconnectIntegrations: 8,
ReferralProgram: 9,
AccountExport: 10,
CloseAccount: 11,
AddFilesToContentStudio: 12,
OptInToReceiveEmailsFromMailchimp: 13,
CreateAudiences: 14,
ViewAudiences: 15,
AudienceExport: 16,
AudienceImport: 17,
AddContacts: 18,
DeleteContacts: 19,
ViewSegments: 20,
EditAudienceSettings: 21,
ArchiveContacts: 22,
CreateOrImportTemplates: 23,
EditTemplates: 24,
CreateEmails: 25,
EditEmails: 26,
SendPublishEmails: 27,
PauseUnpublishEmails: 28,
DeleteEmails: 29,
SubmitSmsMarketingApplication: 30,
CreateSendSmsMmsMessages: 31,
PurchaseSmsCredits: 32,
ViewEmailReports: 33,
ViewSmsReports: 34,
ViewAbuseReports: 35,
ViewEmailStatistics: 36,
UseConversations: 37,
ViewEmailRecipients: 38,
TopLocations: 39,
EmailContactDetails: 40,
EmailOpenDetails: 41,
ECommerceProductActivity: 42,
DomainPerformance: 43,
CreateYourWebsite: 44,
PublishUnpublishYourWebsite: 45,
ViewReport: 46,
CreateALandingPage: 47,
PublishUnpublishALandingPage: 48,
ReplicateALandingPage: 49,
VerifyADomain: 50,
ConnectADomain: 51,
CreateCustomerJourney: 52,
ViewCustomerJourney: 53,
EditCustomerJourney: 54,
TurnOnPauseTurnBackOn: 55,
ViewMessages: 56,
LeaveComments: 57,
SendMessages: 58,
ToggleUserNotifications: 59,
CreateSurvey: 60,
EditSurvey: 61,
PublishSurvey: 62,
DeleteSurvey: 63,
CreateForm: 64,
EditForm: 65,
PublishForm: 66,
DeleteForm: 67,
}
IdToPermission = map[int]Permission{
1: InviteUsers,
2: RevokeAccountAccess,
3: SetUserAccessLevel,
4: Require2FactorAuthentication,
5: ChangeBillingInformation,
6: ChangeCompanyOrganizationName,
7: AddOrAccessApiKeys,
8: CheckReconnectIntegrations,
9: ReferralProgram,
10: AccountExport,
11: CloseAccount,
12: AddFilesToContentStudio,
13: OptInToReceiveEmailsFromMailchimp,
14: CreateAudiences,
15: ViewAudiences,
16: AudienceExport,
17: AudienceImport,
18: AddContacts,
19: DeleteContacts,
20: ViewSegments,
21: EditAudienceSettings,
22: ArchiveContacts,
23: CreateOrImportTemplates,
24: EditTemplates,
25: CreateEmails,
26: EditEmails,
27: SendPublishEmails,
28: PauseUnpublishEmails,
29: DeleteEmails,
30: SubmitSmsMarketingApplication,
31: CreateSendSmsMmsMessages,
32: PurchaseSmsCredits,
33: ViewEmailReports,
34: ViewSmsReports,
35: ViewAbuseReports,
36: ViewEmailStatistics,
37: UseConversations,
38: ViewEmailRecipients,
39: TopLocations,
40: EmailContactDetails,
41: EmailOpenDetails,
42: ECommerceProductActivity,
43: DomainPerformance,
44: CreateYourWebsite,
45: PublishUnpublishYourWebsite,
46: ViewReport,
47: CreateALandingPage,
48: PublishUnpublishALandingPage,
49: ReplicateALandingPage,
50: VerifyADomain,
51: ConnectADomain,
52: CreateCustomerJourney,
53: ViewCustomerJourney,
54: EditCustomerJourney,
55: TurnOnPauseTurnBackOn,
56: ViewMessages,
57: LeaveComments,
58: SendMessages,
59: ToggleUserNotifications,
60: CreateSurvey,
61: EditSurvey,
62: PublishSurvey,
63: DeleteSurvey,
64: CreateForm,
65: EditForm,
66: PublishForm,
67: DeleteForm,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/mailchimp/permissions.yaml
================================================
permissions:
- invite_users
- revoke_account_access
- set_user_access_level
- require_2_factor_authentication
- change_billing_information
- change_company_organization_name
- add_or_access_api_keys
- check_reconnect_integrations
- referral_program
- account_export
- close_account
- add_files_to_content_studio
- opt_in_to_receive_emails_from_mailchimp
- create_audiences
- view_audiences
- audience_export
- audience_import
- add_contacts
- delete_contacts
- view_segments
- edit_audience_settings
- archive_contacts
- create_or_import_templates
- edit_templates
- create_emails
- edit_emails
- send_publish_emails
- pause_unpublish_emails
- delete_emails
- submit_sms_marketing_application
- create_send_sms_mms_messages
- purchase_sms_credits
- view_email_reports
- view_sms_reports
- view_abuse_reports
- view_email_statistics
- use_conversations
- view_email_recipients
- top_locations
- email_contact_details
- email_open_details
- e_commerce_product_activity
- domain_performance
- create_your_website
- publish_unpublish_your_website
- view_report
- create_a_landing_page
- publish_unpublish_a_landing_page
- replicate_a_landing_page
- verify_a_domain
- connect_a_domain
- create_customer_journey
- view_customer_journey
- edit_customer_journey
- turn_on_pause_turn_back_on
- view_messages
- leave_comments
- send_messages
- toggle_user_notifications
- create_survey
- edit_survey
- publish_survey
- delete_survey
- create_form
- edit_form
- publish_form
- delete_form
================================================
FILE: pkg/analyzer/analyzers/mailgun/expected_output.json
================================================
{
"AnalyzerType": 8,
"Bindings": [
{
"Resource": {
"Name": "sandbox19e49763d44e498e850589ea7d54bd82.mailgun.org",
"FullyQualifiedName": "mailgun/6478cb31d026c112819856cd/sandbox19e49763d44e498e850589ea7d54bd82.mailgun.org",
"Type": "domain",
"Metadata": {
"created_at": "Thu, 01 Jun 2023 16:45:37 GMT",
"is_disabled": false,
"state": "active",
"type": "sandbox"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": null
}
================================================
FILE: pkg/analyzer/analyzers/mailgun/mailgun.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go mailgun
package mailgun
import (
"errors"
"os"
"strconv"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
type SecretInfo struct {
ID string // key id
UserName string
Type string // type of key
Role string // key role
ExpiresAt string // key expiry time if any
Domains []Domain
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeMailgun }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, errors.New("key not found in credentialInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeMailgun,
Bindings: make([]analyzers.Binding, len(info.Domains)),
}
for idx, domain := range info.Domains {
result.Bindings[idx] = analyzers.Binding{
Resource: analyzers.Resource{
Name: domain.URL,
FullyQualifiedName: "mailgun/" + domain.ID + "/" + domain.URL,
Type: "domain",
Metadata: map[string]any{
"created_at": domain.CreatedAt,
"type": domain.Type,
"state": domain.State,
"is_disabled": domain.IsDisabled,
},
},
Permission: analyzers.Permission{
Value: PermissionStrings[FullAccess],
},
}
}
return &result
}
func AnalyzeAndPrintPermissions(cfg *config.Config, apiKey string) {
info, err := AnalyzePermissions(cfg, apiKey)
if err != nil {
color.Red("[x] %s", err.Error())
return
}
color.Green("[i] Valid Mailgun API key\n\n")
printKeyInfo(info)
printDomains(info.Domains)
color.Yellow("[i] Permissions: Full Access\n\n")
}
func AnalyzePermissions(cfg *config.Config, apiKey string) (*SecretInfo, error) {
var secretInfo SecretInfo
var client = analyzers.NewAnalyzeClient(cfg)
if err := getDomains(client, apiKey, &secretInfo); err != nil {
return &secretInfo, err
}
if err := getKeys(client, apiKey, &secretInfo); err != nil {
return &secretInfo, err
}
return &secretInfo, nil
}
func printKeyInfo(info *SecretInfo) {
if info.ID == "" {
color.Red("[i] Key information not found")
return
}
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Key ID", "UserName/Requester", "Key Type", "Expires At", "Role"})
t.AppendRow(table.Row{info.ID, info.UserName, info.Type, info.ExpiresAt, info.Role})
t.Render()
}
func printDomains(domains []Domain) {
if len(domains) == 0 {
color.Red("[i] No domains found")
return
}
color.Yellow("[i] Found %d domain(s)", len(domains))
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Domain", "Type", "State", "Created At", "Disabled"})
for _, domain := range domains {
var colorFunc func(format string, a ...interface{}) string
switch {
case domain.IsDisabled:
colorFunc = color.RedString
case domain.Type == "sandbox" || domain.State == "unverified":
colorFunc = color.YellowString
default:
colorFunc = color.GreenString
}
t.AppendRow([]interface{}{
colorFunc(domain.URL),
colorFunc(domain.Type),
colorFunc(domain.State),
colorFunc(domain.CreatedAt),
colorFunc(strconv.FormatBool(domain.IsDisabled)),
})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/mailgun/mailgun_test.go
================================================
package mailgun
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(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)
}
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid Mailgun key",
key: testSecrets.MustGetField("NEW_MAILGUN_TOKEN_ACTIVE"),
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/mailgun/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package mailgun
import "errors"
type Permission int
const (
Invalid Permission = iota
Read Permission = iota
Write Permission = iota
FullAccess Permission = iota
)
var (
PermissionStrings = map[Permission]string{
Read: "read",
Write: "write",
FullAccess: "full_access",
}
StringToPermission = map[string]Permission{
"read": Read,
"write": Write,
"full_access": FullAccess,
}
PermissionIDs = map[Permission]int{
Read: 1,
Write: 2,
FullAccess: 3,
}
IdToPermission = map[int]Permission{
1: Read,
2: Write,
3: FullAccess,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/mailgun/permissions.yaml
================================================
permissions:
- read
- write
- full_access
================================================
FILE: pkg/analyzer/analyzers/mailgun/requests.go
================================================
package mailgun
import (
"encoding/json"
"fmt"
"net/http"
"strings"
)
// DomainsJSON is /domains API response
type DomainsJSON struct {
Items []Domain `json:"items"`
TotalCount int `json:"total_count"`
}
// Domain is a single mailgun domain details
type Domain struct {
ID string `json:"id"`
URL string `json:"name"`
IsDisabled bool `json:"is_disabled"`
Type string `json:"type"`
State string `json:"state"`
CreatedAt string `json:"created_at"`
}
// KeysJSON is /v1/keys API response
type KeysJSON struct {
Items []Key `json:"items"`
TotalCount int `json:"total_count"`
}
// Key is a single mailgun Key details
type Key struct {
ID string `json:"id"`
Requester string `json:"requestor"`
UserName string `json:"user_name"`
Role string `json:"role"`
Type string `json:"kind"`
ExpiresAt string `json:"expires_at"`
}
// getDomains list all domains
func getDomains(client *http.Client, apiKey string, secretInfo *SecretInfo) error {
var domainsJSON DomainsJSON
req, err := http.NewRequest("GET", "https://api.mailgun.net/v4/domains", nil)
if err != nil {
return err
}
req.SetBasicAuth("api", apiKey)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("invalid Mailgun API key")
}
err = json.NewDecoder(resp.Body).Decode(&domainsJSON)
if err != nil {
return err
}
// populate secretInfo with domains
secretInfo.Domains = append(secretInfo.Domains, domainsJSON.Items...)
return nil
}
func getKeys(client *http.Client, apiKey string, secretInfo *SecretInfo) error {
var keysJSON KeysJSON
req, err := http.NewRequest("GET", "https://api.mailgun.net/v1/keys", nil)
if err != nil {
return err
}
req.SetBasicAuth("api", apiKey)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("invalid Mailgun API key")
}
err = json.NewDecoder(resp.Body).Decode(&keysJSON)
if err != nil {
return err
}
// populate secretInfo with key details
for _, key := range keysJSON.Items {
// filter the exact key which we are analyzing
// ID is actually the suffix of actual apiKeys
if strings.Contains(apiKey, key.ID) {
keyToSecretInfo(key, secretInfo)
}
}
return nil
}
func keyToSecretInfo(key Key, secretInfo *SecretInfo) {
secretInfo.ID = key.ID
if key.UserName != "" {
secretInfo.UserName = key.UserName
} else {
secretInfo.UserName = key.Requester
}
secretInfo.Role = key.Role
secretInfo.Type = key.Type
if secretInfo.ExpiresAt != "" {
secretInfo.ExpiresAt = key.ExpiresAt
} else {
secretInfo.ExpiresAt = "Never"
}
}
================================================
FILE: pkg/analyzer/analyzers/monday/monday.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go monday
package monday
import (
"errors"
"fmt"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
type SecretInfo struct {
User Me
Account Account
Resources []MondayResource
}
func (s *SecretInfo) appendResource(resource MondayResource) {
s.Resources = append(s.Resources, resource)
}
type MondayResource struct {
ID string
Name string
Type string
MetaData map[string]string
Parent *MondayResource
}
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeMonday
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, exist := credInfo["key"]
if !exist {
return nil, errors.New("key not found in credentials info")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
// just print the error in cli and continue as a partial success
color.Red("[x] Error : %s", err.Error())
}
if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}
color.Green("[!] Valid Monday Personal Access Token\n\n")
// print user information
printUser(info.User)
printResources(info.Resources)
color.Yellow("\n[i] Expires: Never")
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
// create http client
client := analyzers.NewAnalyzeClientUnrestricted(cfg)
var secretInfo = &SecretInfo{}
// captureMondayData make a query to graphql API of monday to fetch all data and store it in secret info
if err := captureMondayData(client, key, secretInfo); err != nil {
return nil, err
}
return secretInfo, nil
}
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeMonday,
Metadata: map[string]any{},
Bindings: make([]analyzers.Binding, 0),
}
// extract information from resource to create bindings and append to result bindings
for _, resource := range info.Resources {
binding := analyzers.Binding{
Resource: analyzers.Resource{
Name: resource.Name,
FullyQualifiedName: fmt.Sprintf("%s/%s", resource.Type, resource.ID), // e.g: Board/123
Type: resource.Type,
Metadata: map[string]any{}, // to avoid panic
},
Permission: analyzers.Permission{
Value: PermissionStrings[FullAccess], // token always has full access
},
}
for key, value := range resource.MetaData {
binding.Resource.Metadata[key] = value
}
result.Bindings = append(result.Bindings, binding)
}
return &result
}
// cli print functions
func printUser(user Me) {
color.Green("\n[i] User Information:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"ID", "Name", "Email", "Title", "Is Admin", "Is Guest"})
t.AppendRow(table.Row{color.GreenString(user.ID), color.GreenString(user.Name), color.GreenString(user.Email),
color.GreenString(user.Title), color.GreenString(fmt.Sprintf("%t", user.IsAdmin)), color.GreenString(fmt.Sprintf("%t", user.IsGuest))})
t.Render()
}
func printResources(resources []MondayResource) {
color.Green("\n[i] Resources:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Name", "Type"})
for _, resource := range resources {
t.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/monday/monday_test.go
================================================
package monday
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed result_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "analyzers1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
key := testSecrets.MustGetField("MONDAY_PAT")
tests := []struct {
name string
key string
want []byte // JSON string
wantErr bool
}{
{
name: "valid monday personal access token",
key: key,
want: expectedOutput,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/monday/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package monday
import "errors"
type Permission int
const (
Invalid Permission = iota
FullAccess Permission = iota
)
var (
PermissionStrings = map[Permission]string{
FullAccess: "full_access",
}
StringToPermission = map[string]Permission{
"full_access": FullAccess,
}
PermissionIDs = map[Permission]int{
FullAccess: 1,
}
IdToPermission = map[int]Permission{
1: FullAccess,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/monday/permissions.yaml
================================================
permissions:
- full_access
================================================
FILE: pkg/analyzer/analyzers/monday/query.go
================================================
package monday
import (
"bytes"
_ "embed"
"encoding/json"
"fmt"
"io"
"net/http"
)
//go:embed query.graphql
var requestQuery string
const (
// resource types
TypeBoard = "Board"
TypeBoardGroup = "Board Group"
TypeBoardColumn = "Board Column"
TypeDoc = "Document"
TypeFolder = "Folder"
TypeTag = "Tag"
TypeTeam = "Team"
TypeWorkspace = "Workspace"
)
type Request struct {
Query string `json:"query"`
}
// Response is the Monday Graphql API response in case of success
type Response struct {
Data Data `json:"data"`
}
type Data struct {
Me Me `json:"me"`
Account Account `json:"account"`
Users []User `json:"users"`
Boards []Board `json:"boards"`
Docs []Doc `json:"docs"`
Folders []EntityRef `json:"folders"`
Tags []EntityRef `json:"tags"`
Teams []EntityRef `json:"teams"`
Workspaces []Workspace `json:"workspaces"`
}
type EntityRef struct {
ID string `json:"id"`
Name string `json:"name"`
}
type Me struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Title string `json:"title"`
IsAdmin bool `json:"is_admin"`
IsGuest bool `json:"is_guest"`
IsViewOnly bool `json:"is_view_only"`
IsPending bool `json:"is_pending"`
IsVerified bool `json:"is_verified"`
Teams []EntityRef `json:"teams"`
}
type Account struct {
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Tier string `json:"tier"`
}
type User struct {
Email string `json:"email"`
Account Account `json:"account"`
}
type Board struct {
ID string `json:"id"`
Name string `json:"name"`
State string `json:"state"`
Permissions string `json:"permissions"`
Groups []Group `json:"groups"`
Columns []Column `json:"column"`
Owners []EntityRef `json:"owner"`
}
type Group struct {
Title string `json:"title"`
ID string `json:"id"`
}
type Column struct {
ID string `json:"id"`
Title string `json:"title"`
Type string `json:"type"`
}
type Doc struct {
ID string `json:"id"`
ObjectID string `json:"object_id"`
Name string `json:"name"`
CreatedBy EntityRef `json:"created_by"`
}
type Workspace struct {
ID string `json:"id"`
Name string `json:"name"`
Kind string `json:"kind"`
}
// captureMondayData send a request to Monday graphql API to get all data and capture it in secret info
func captureMondayData(client *http.Client, key string, secretInfo *SecretInfo) error {
jsonData, err := json.Marshal(Request{Query: requestQuery})
if err != nil {
panic(err)
}
req, err := http.NewRequest(http.MethodPost, "https://api.monday.com/v2", bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", key)
resp, err := client.Do(req)
if err != nil {
return err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
var apiResponse Response
if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
return err
}
// capture details in secret info
responseToSecretInfo(apiResponse, secretInfo)
return nil
case http.StatusUnauthorized:
return fmt.Errorf("expired/invalid access token")
default:
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
// responseToSecretInfo translate api response to secret info
func responseToSecretInfo(apiResponse Response, secretInfo *SecretInfo) {
secretInfo.User = apiResponse.Data.Me
secretInfo.Account = apiResponse.Data.Account
processBoards(apiResponse.Data.Boards, secretInfo)
processDocs(apiResponse.Data.Docs, secretInfo)
processSimpleEntities(apiResponse.Data.Folders, TypeFolder, secretInfo)
processSimpleEntities(apiResponse.Data.Tags, TypeTag, secretInfo)
processSimpleEntities(apiResponse.Data.Teams, TypeTeam, secretInfo)
processWorkspaces(apiResponse.Data.Workspaces, secretInfo)
}
func processBoards(boards []Board, secretInfo *SecretInfo) {
for _, board := range boards {
boardResource := MondayResource{
ID: board.ID,
Name: board.Name,
Type: TypeBoard,
MetaData: map[string]string{
"state": board.State,
"permissions": board.Permissions,
},
}
secretInfo.appendResource(boardResource)
// sub resources of board
for _, group := range board.Groups {
secretInfo.appendResource(MondayResource{
ID: group.ID,
Name: group.Title,
Type: TypeBoardGroup,
Parent: &boardResource,
})
}
for _, column := range board.Columns {
secretInfo.appendResource(MondayResource{
ID: column.ID,
Name: column.Title,
Type: TypeBoardColumn,
MetaData: map[string]string{
"Column Type": column.Type,
},
Parent: &boardResource,
})
}
}
}
func processDocs(docs []Doc, secretInfo *SecretInfo) {
for _, doc := range docs {
secretInfo.appendResource(MondayResource{
ID: doc.ID,
Name: doc.Name,
Type: TypeDoc,
MetaData: map[string]string{
"created_by": doc.CreatedBy.Name,
},
})
}
}
func processSimpleEntities(entities []EntityRef, entityType string, secretInfo *SecretInfo) {
for _, entity := range entities {
secretInfo.appendResource(MondayResource{
ID: entity.ID,
Name: entity.Name,
Type: entityType,
})
}
}
func processWorkspaces(workspaces []Workspace, secretInfo *SecretInfo) {
for _, workspace := range workspaces {
secretInfo.appendResource(MondayResource{
ID: workspace.ID,
Name: workspace.Name,
Type: TypeWorkspace,
MetaData: map[string]string{
"workspace_kind": workspace.Kind,
},
})
}
}
================================================
FILE: pkg/analyzer/analyzers/monday/query.graphql
================================================
{
me {
id
name
email
title
is_admin
is_guest
is_view_only
is_pending
is_verified
teams {
id
name
}
}
account {
id
name
slug
tier
}
users {
email
account {
name
id
}
}
boards {
id
name
state
permissions
groups {
title
id
}
columns {
id
title
type
}
owners {
id
name
}
}
docs {
id
object_id
name
created_by {
id
name
}
}
folders {
name
id
}
tags {
id
name
}
teams {
id
name
}
workspaces {
id
name
kind
}
}
================================================
FILE: pkg/analyzer/analyzers/monday/result_output.json
================================================
{
"AnalyzerType": 35,
"Bindings": [
{
"Resource": {
"Name": "All Tasks",
"FullyQualifiedName": "Board Group/topics",
"Type": "Board Group",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Bugs Queue",
"FullyQualifiedName": "Board/2007387485",
"Type": "Board",
"Metadata": {
"permissions": "everyone",
"state": "active"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Development Work",
"FullyQualifiedName": "Board Group/new_group24572",
"Type": "Board Group",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Epics",
"FullyQualifiedName": "Board/2007387484",
"Type": "Board",
"Metadata": {
"permissions": "everyone",
"state": "active"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Epics Backlog",
"FullyQualifiedName": "Board Group/new_group",
"Type": "Board Group",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Getting Started",
"FullyQualifiedName": "Board/2007387480",
"Type": "Board",
"Metadata": {
"permissions": "everyone",
"state": "active"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Getting Started",
"FullyQualifiedName": "Document/1126907",
"Type": "Document",
"Metadata": {
"created_by": "Truffle Security Detectors"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Group Title",
"FullyQualifiedName": "Board Group/topics",
"Type": "Board Group",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Incoming Bugs",
"FullyQualifiedName": "Board Group/topics",
"Type": "Board Group",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Managed in sprints",
"FullyQualifiedName": "Board Group/new_group",
"Type": "Board Group",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "My Team",
"FullyQualifiedName": "Workspace/1857558",
"Type": "Workspace",
"Metadata": {
"workspace_kind": "open"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Q1 2025",
"FullyQualifiedName": "Board Group/new_group313",
"Type": "Board Group",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Resolved",
"FullyQualifiedName": "Board Group/group_title",
"Type": "Board Group",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Retrospectives",
"FullyQualifiedName": "Board/2007387481",
"Type": "Board",
"Metadata": {
"permissions": "everyone",
"state": "active"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Sprint 1",
"FullyQualifiedName": "Board Group/group_title",
"Type": "Board Group",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Sprints",
"FullyQualifiedName": "Board Group/topics",
"Type": "Board Group",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Sprints",
"FullyQualifiedName": "Board/2007387482",
"Type": "Board",
"Metadata": {
"permissions": "everyone",
"state": "active"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Sprints order",
"FullyQualifiedName": "Board/2007387483",
"Type": "Board",
"Metadata": {
"permissions": "everyone",
"state": "active"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Team OSS team",
"FullyQualifiedName": "Folder/6205823",
"Type": "Folder",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": {}
}
================================================
FILE: pkg/analyzer/analyzers/mux/expected_output.json
================================================
{"AnalyzerType":38,"Bindings":[{"Resource":{"Name":"wV300mH02AsW9XmwfieoMlmLNXConYCREXQHbb7kWAUbw","FullyQualifiedName":"asset/SnmkoMLmA2smTuCHU18H00MOL028LeVQHX3uEQCwD7BMI/track/wV300mH02AsW9XmwfieoMlmLNXConYCREXQHbb7kWAUbw","Type":"track","Metadata":{"duration":16.750067,"languageCode":"","maxChannels":0,"maxFrameRate":29.97,"maxHeight":1080,"maxWidth":1920,"name":"","primary":false,"status":"","textSource":"","textType":"","type":"video"},"Parent":{"Name":"SnmkoMLmA2smTuCHU18H00MOL028LeVQHX3uEQCwD7BMI","FullyQualifiedName":"asset/SnmkoMLmA2smTuCHU18H00MOL028LeVQHX3uEQCwD7BMI","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746529086","duration":16.750067,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null}},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"6gYp01Q1DQu02Y1BFChIGYlEReYtMyZWYC601VcrSLK02KA","FullyQualifiedName":"asset/SnmkoMLmA2smTuCHU18H00MOL028LeVQHX3uEQCwD7BMI/playback_id/6gYp01Q1DQu02Y1BFChIGYlEReYtMyZWYC601VcrSLK02KA","Type":"playback_id","Metadata":{"policy":"public"},"Parent":{"Name":"SnmkoMLmA2smTuCHU18H00MOL028LeVQHX3uEQCwD7BMI","FullyQualifiedName":"asset/SnmkoMLmA2smTuCHU18H00MOL028LeVQHX3uEQCwD7BMI","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746529086","duration":16.750067,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null}},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"SnmkoMLmA2smTuCHU18H00MOL028LeVQHX3uEQCwD7BMI","FullyQualifiedName":"asset/SnmkoMLmA2smTuCHU18H00MOL028LeVQHX3uEQCwD7BMI","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746529086","duration":16.750067,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"EPWJi6t301mELwzilEDAnZS8T2Uqs8ULDbgZVeCOhLNA","FullyQualifiedName":"asset/a00U5GeBR6pAO3MkdZQBB00enwG1rLwx7UTsYPd0200sylo/track/EPWJi6t301mELwzilEDAnZS8T2Uqs8ULDbgZVeCOhLNA","Type":"track","Metadata":{"duration":16.750067,"languageCode":"","maxChannels":0,"maxFrameRate":29.97,"maxHeight":1080,"maxWidth":1920,"name":"","primary":false,"status":"","textSource":"","textType":"","type":"video"},"Parent":{"Name":"a00U5GeBR6pAO3MkdZQBB00enwG1rLwx7UTsYPd0200sylo","FullyQualifiedName":"asset/a00U5GeBR6pAO3MkdZQBB00enwG1rLwx7UTsYPd0200sylo","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746529083","duration":16.750067,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null}},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"X9gY7SmIrDIB5Y02gu8KnUnzuAOi005iOafaJuCQqqZbA","FullyQualifiedName":"asset/a00U5GeBR6pAO3MkdZQBB00enwG1rLwx7UTsYPd0200sylo/playback_id/X9gY7SmIrDIB5Y02gu8KnUnzuAOi005iOafaJuCQqqZbA","Type":"playback_id","Metadata":{"policy":"public"},"Parent":{"Name":"a00U5GeBR6pAO3MkdZQBB00enwG1rLwx7UTsYPd0200sylo","FullyQualifiedName":"asset/a00U5GeBR6pAO3MkdZQBB00enwG1rLwx7UTsYPd0200sylo","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746529083","duration":16.750067,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null}},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"a00U5GeBR6pAO3MkdZQBB00enwG1rLwx7UTsYPd0200sylo","FullyQualifiedName":"asset/a00U5GeBR6pAO3MkdZQBB00enwG1rLwx7UTsYPd0200sylo","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746529083","duration":16.750067,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"6nV01pBVrQLad3kjFoEUd023Uxfgl8x8DOK546dqA5xD00","FullyQualifiedName":"asset/QLnXn72rbNKLLvfzVJU2t01atuGSbyYMLsU026jza8YOw/track/6nV01pBVrQLad3kjFoEUd023Uxfgl8x8DOK546dqA5xD00","Type":"track","Metadata":{"duration":25.45,"languageCode":"","maxChannels":2,"maxFrameRate":0,"maxHeight":0,"maxWidth":0,"name":"","primary":true,"status":"","textSource":"","textType":"","type":"audio"},"Parent":{"Name":"QLnXn72rbNKLLvfzVJU2t01atuGSbyYMLsU026jza8YOw","FullyQualifiedName":"asset/QLnXn72rbNKLLvfzVJU2t01atuGSbyYMLsU026jza8YOw","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746513418","duration":25.492133,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null}},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"OXOWt16AiGYvtAwuFFfA00hKAHNW02ERja00bvPEWmHLys","FullyQualifiedName":"asset/QLnXn72rbNKLLvfzVJU2t01atuGSbyYMLsU026jza8YOw/track/OXOWt16AiGYvtAwuFFfA00hKAHNW02ERja00bvPEWmHLys","Type":"track","Metadata":{"duration":25.4254,"languageCode":"","maxChannels":0,"maxFrameRate":29.97,"maxHeight":1080,"maxWidth":1920,"name":"","primary":false,"status":"","textSource":"","textType":"","type":"video"},"Parent":{"Name":"QLnXn72rbNKLLvfzVJU2t01atuGSbyYMLsU026jza8YOw","FullyQualifiedName":"asset/QLnXn72rbNKLLvfzVJU2t01atuGSbyYMLsU026jza8YOw","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746513418","duration":25.492133,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null}},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"gaJcYKWQ7P01r02XJwU02KAe5KofkQ01497weVghrWNrqlWNYAXHx7fqmQ","FullyQualifiedName":"asset/QLnXn72rbNKLLvfzVJU2t01atuGSbyYMLsU026jza8YOw/track/gaJcYKWQ7P01r02XJwU02KAe5KofkQ01497weVghrWNrqlWNYAXHx7fqmQ","Type":"track","Metadata":{"duration":0,"languageCode":"pt","maxChannels":0,"maxFrameRate":0,"maxHeight":0,"maxWidth":0,"name":"Portuguese","primary":false,"status":"ready","textSource":"generated_vod","textType":"subtitles","type":"text"},"Parent":{"Name":"QLnXn72rbNKLLvfzVJU2t01atuGSbyYMLsU026jza8YOw","FullyQualifiedName":"asset/QLnXn72rbNKLLvfzVJU2t01atuGSbyYMLsU026jza8YOw","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746513418","duration":25.492133,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null}},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"hQKQ8U1RkGTN3700ynnDCa00y4q2sCflbKf2Nw01T8OcTc","FullyQualifiedName":"asset/QLnXn72rbNKLLvfzVJU2t01atuGSbyYMLsU026jza8YOw/playback_id/hQKQ8U1RkGTN3700ynnDCa00y4q2sCflbKf2Nw01T8OcTc","Type":"playback_id","Metadata":{"policy":"signed"},"Parent":{"Name":"QLnXn72rbNKLLvfzVJU2t01atuGSbyYMLsU026jza8YOw","FullyQualifiedName":"asset/QLnXn72rbNKLLvfzVJU2t01atuGSbyYMLsU026jza8YOw","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746513418","duration":25.492133,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null}},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"QLnXn72rbNKLLvfzVJU2t01atuGSbyYMLsU026jza8YOw","FullyQualifiedName":"asset/QLnXn72rbNKLLvfzVJU2t01atuGSbyYMLsU026jza8YOw","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746513418","duration":25.492133,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"ZJ9HiAaXUO02Da3W7Yj3y5Ct902SIafAbjMTvDhnfaOcs","FullyQualifiedName":"asset/Wa5Qmq78b02p2kMCB3P7Lyn4tHjiSkEJT01HEZq8l4Oq8/track/ZJ9HiAaXUO02Da3W7Yj3y5Ct902SIafAbjMTvDhnfaOcs","Type":"track","Metadata":{"duration":25.4254,"languageCode":"","maxChannels":0,"maxFrameRate":29.97,"maxHeight":1080,"maxWidth":1920,"name":"","primary":false,"status":"","textSource":"","textType":"","type":"video"},"Parent":{"Name":"Wa5Qmq78b02p2kMCB3P7Lyn4tHjiSkEJT01HEZq8l4Oq8","FullyQualifiedName":"asset/Wa5Qmq78b02p2kMCB3P7Lyn4tHjiSkEJT01HEZq8l4Oq8","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746513375","duration":25.492133,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null}},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"5Od27uOFUUNPgNhnqwxmc6YQH200q5SD17CRkc25eciM6YMb7c00JvDA","FullyQualifiedName":"asset/Wa5Qmq78b02p2kMCB3P7Lyn4tHjiSkEJT01HEZq8l4Oq8/track/5Od27uOFUUNPgNhnqwxmc6YQH200q5SD17CRkc25eciM6YMb7c00JvDA","Type":"track","Metadata":{"duration":0,"languageCode":"it","maxChannels":0,"maxFrameRate":0,"maxHeight":0,"maxWidth":0,"name":"Italian","primary":false,"status":"ready","textSource":"generated_vod","textType":"subtitles","type":"text"},"Parent":{"Name":"Wa5Qmq78b02p2kMCB3P7Lyn4tHjiSkEJT01HEZq8l4Oq8","FullyQualifiedName":"asset/Wa5Qmq78b02p2kMCB3P7Lyn4tHjiSkEJT01HEZq8l4Oq8","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746513375","duration":25.492133,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null}},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"xmgPZVZwnFlWC8Y2Q0046eAOxR88oPP01S5OqHYPLBM01jy601502OoGSwA","FullyQualifiedName":"asset/Wa5Qmq78b02p2kMCB3P7Lyn4tHjiSkEJT01HEZq8l4Oq8/track/xmgPZVZwnFlWC8Y2Q0046eAOxR88oPP01S5OqHYPLBM01jy601502OoGSwA","Type":"track","Metadata":{"duration":0,"languageCode":"und","maxChannels":2,"maxFrameRate":0,"maxHeight":0,"maxWidth":0,"name":"Default","primary":true,"status":"ready","textSource":"","textType":"","type":"audio"},"Parent":{"Name":"Wa5Qmq78b02p2kMCB3P7Lyn4tHjiSkEJT01HEZq8l4Oq8","FullyQualifiedName":"asset/Wa5Qmq78b02p2kMCB3P7Lyn4tHjiSkEJT01HEZq8l4Oq8","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746513375","duration":25.492133,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null}},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"GD1K9sPH4Vopr4ticPdOAO02vEIslfN2400cPQnA8YZfo","FullyQualifiedName":"asset/Wa5Qmq78b02p2kMCB3P7Lyn4tHjiSkEJT01HEZq8l4Oq8/playback_id/GD1K9sPH4Vopr4ticPdOAO02vEIslfN2400cPQnA8YZfo","Type":"playback_id","Metadata":{"policy":"public"},"Parent":{"Name":"Wa5Qmq78b02p2kMCB3P7Lyn4tHjiSkEJT01HEZq8l4Oq8","FullyQualifiedName":"asset/Wa5Qmq78b02p2kMCB3P7Lyn4tHjiSkEJT01HEZq8l4Oq8","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746513375","duration":25.492133,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null}},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"Wa5Qmq78b02p2kMCB3P7Lyn4tHjiSkEJT01HEZq8l4Oq8","FullyQualifiedName":"asset/Wa5Qmq78b02p2kMCB3P7Lyn4tHjiSkEJT01HEZq8l4Oq8","Type":"asset","Metadata":{"aspectRatio":"16:9","createdAt":"1746513375","duration":25.492133,"mp4Support":"none","status":"ready","videoQuality":"basic"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"8f23e2ab-6780-4dc8-aa8d-8e27339188a6","FullyQualifiedName":"annotation/8f23e2ab-6780-4dc8-aa8d-8e27339188a6","Type":"annotation","Metadata":{"date":"2025-04-23T20:00:00Z","note":"This is a note2","subPropertyID":"123456"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"21d0aca6-9ea4-4d92-a8c5-254a7da2a455","FullyQualifiedName":"annotation/21d0aca6-9ea4-4d92-a8c5-254a7da2a455","Type":"annotation","Metadata":{"date":"2025-04-23T20:00:00Z","note":"This is a note","subPropertyID":"123456"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"NNqGrk9T7Bi4AkaWB38JoOEJiUb417f01P43agSLYXCg","FullyQualifiedName":"signing_key/NNqGrk9T7Bi4AkaWB38JoOEJiUb417f01P43agSLYXCg","Type":"signing_key","Metadata":{"createdAt":"1746615294"},"Parent":null},"Permission":{"Value":"read","Parent":null}},{"Resource":{"Name":"t4zt5HNbEPmJEDJLpLMXty4d39xMlMgB0292mlbY17sI","FullyQualifiedName":"signing_key/t4zt5HNbEPmJEDJLpLMXty4d39xMlMgB0292mlbY17sI","Type":"signing_key","Metadata":{"createdAt":"1746615296"},"Parent":null},"Permission":{"Value":"read","Parent":null}},{"Resource":{"Name":"dGQ8BK2joovz4WElQqK02I9ZXN1UeySO27Zn21Y8ATf8","FullyQualifiedName":"signing_key/dGQ8BK2joovz4WElQqK02I9ZXN1UeySO27Zn21Y8ATf8","Type":"signing_key","Metadata":{"createdAt":"1746615298"},"Parent":null},"Permission":{"Value":"read","Parent":null}},{"Resource":{"Name":"3FA1JvJkG8Ye4i4OpTRcEvFkuNVWkeTLN8BEkKWUko8","FullyQualifiedName":"signing_key/3FA1JvJkG8Ye4i4OpTRcEvFkuNVWkeTLN8BEkKWUko8","Type":"signing_key","Metadata":{"createdAt":"1746615300"},"Parent":null},"Permission":{"Value":"read","Parent":null}},{"Resource":{"Name":"7kH3cGnX4e5qavqA1Zd7GbxeQ7pDCX702LuXUhCOhdnY","FullyQualifiedName":"signing_key/7kH3cGnX4e5qavqA1Zd7GbxeQ7pDCX702LuXUhCOhdnY","Type":"signing_key","Metadata":{"createdAt":"1746615302"},"Parent":null},"Permission":{"Value":"read","Parent":null}},{"Resource":{"Name":"T6OmB00xJqpIoM3nbSTRbvHiOjrTGCA7HkKx02UtRRkgk","FullyQualifiedName":"signing_key/T6OmB00xJqpIoM3nbSTRbvHiOjrTGCA7HkKx02UtRRkgk","Type":"signing_key","Metadata":{"createdAt":"1746615304"},"Parent":null},"Permission":{"Value":"read","Parent":null}},{"Resource":{"Name":"ivN5HCZBsuhWUUBrlusVCB7aseh87N1sAji7XFM8LEs","FullyQualifiedName":"signing_key/ivN5HCZBsuhWUUBrlusVCB7aseh87N1sAji7XFM8LEs","Type":"signing_key","Metadata":{"createdAt":"1746615305"},"Parent":null},"Permission":{"Value":"read","Parent":null}},{"Resource":{"Name":"jUGFLfsJSqDev6NBjbnOLh4VX6WZJTIuHSFAgomgpkQ","FullyQualifiedName":"signing_key/jUGFLfsJSqDev6NBjbnOLh4VX6WZJTIuHSFAgomgpkQ","Type":"signing_key","Metadata":{"createdAt":"1746615307"},"Parent":null},"Permission":{"Value":"read","Parent":null}},{"Resource":{"Name":"xnM4650153BqZfmYf167G8Vo91X9Z43im024AwUksPP7o","FullyQualifiedName":"signing_key/xnM4650153BqZfmYf167G8Vo91X9Z43im024AwUksPP7o","Type":"signing_key","Metadata":{"createdAt":"1746615327"},"Parent":null},"Permission":{"Value":"read","Parent":null}}],"UnboundedResources":null,"Metadata":null}
================================================
FILE: pkg/analyzer/analyzers/mux/models.go
================================================
package mux
import (
"fmt"
"net/http"
)
type ResourceType string
const (
ResourceTypeVideo ResourceType = "video"
ResourceTypeData ResourceType = "data"
ResourceTypeSystem ResourceType = "system"
)
type permissionTestConfig struct {
Tests []permissionTest `json:"tests"`
}
type permissionTest struct {
ResourceType ResourceType `json:"resource_type"`
Permission string `json:"permission"`
Endpoint string `json:"endpoint"`
Method string `json:"method"`
ValidStatusCode int `json:"valid_status_code"`
}
func (test permissionTest) testPermission(client *http.Client, key string, secret string) (bool, error) {
_, statusCode, err := makeAPIRequest(client, key, secret, test.Method, test.Endpoint)
if err != nil {
return false, err
}
switch statusCode {
case test.ValidStatusCode:
return true, nil
case http.StatusNotFound:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", statusCode)
}
}
type secretInfo struct {
Permissions map[ResourceType]Permission
Assets []asset
Annotations []annotation
SigningKeys []signingKey
}
func (info *secretInfo) addPermission(resourceType ResourceType, permission string) {
if info.Permissions == nil {
info.Permissions = map[ResourceType]Permission{}
}
if perm := info.Permissions[resourceType]; perm == FullAccess {
return
}
if permission == "read" {
info.Permissions[resourceType] = Read
} else if permission == "write" {
info.Permissions[resourceType] = FullAccess
}
}
func (info *secretInfo) hasPermission(resourceType ResourceType, permission Permission) bool {
perm, exists := info.Permissions[resourceType]
if !exists {
return false
}
return perm == permission || perm == FullAccess
}
// Resource structs
type track struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Duration float64 `json:"duration"`
Status string `json:"status"`
Primary bool `json:"primary"`
TextType string `json:"text_type"`
TextSource string `json:"text_source"`
LanguageCode string `json:"language_code"`
MaxWidth int `json:"max_width"`
MaxHeight int `json:"max_height"`
MaxFrameRate float64 `json:"max_frame_rate"`
MaxChannels int `json:"max_channels"`
}
type playbackID struct {
ID string `json:"id"`
Policy string `json:"policy"`
}
type meta struct {
Title string `json:"title"`
ExternalID string `json:"external_id"`
CreatorID string `json:"creator_id"`
}
type asset struct {
ID string `json:"id"`
Duration float64 `json:"duration"`
Status string `json:"status"`
VideoQuality string `json:"video_quality"`
MP4Support string `json:"mp4_support"`
AspectRatio string `json:"aspect_ratio"`
Tracks []track `json:"tracks"`
PlaybackIDs []playbackID `json:"playback_ids"`
Meta meta `json:"meta"`
CreatedAt string `json:"created_at"`
}
type annotation struct {
SubPropertyID string `json:"sub_property_id"`
Note string `json:"note"`
ID string `json:"id"`
Date string `json:"date"`
}
type signingKey struct {
ID string `json:"id"`
CreatedAt string `json:"created_at"`
}
// API response structs
type assetListResponse struct {
Data []asset `json:"data"`
}
type annotationListResponse struct {
Data []annotation `json:"data"`
}
type signingKeyListResponse struct {
Data []signingKey `json:"data"`
}
================================================
FILE: pkg/analyzer/analyzers/mux/mux.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go mux
package mux
import (
"encoding/json"
"errors"
"fmt"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
_ "embed"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
//go:embed tests.json
var testsConfig []byte
func readTestsConfig() (*permissionTestConfig, error) {
var config permissionTestConfig
if err := json.Unmarshal(testsConfig, &config); err != nil {
return nil, fmt.Errorf("failed to unmarshal tests config: %w", err)
}
return &config, nil
}
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeMux
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, exist := credInfo["key"]
if !exist {
return nil, errors.New("key not found in credentials info")
}
secret, exist := credInfo["secret"]
if !exist {
return nil, errors.New("secret not found in credentials info")
}
info, err := AnalyzePermissions(a.Cfg, key, secret)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string, secret string) {
info, err := AnalyzePermissions(cfg, key, secret)
if err != nil {
color.Red("[x] Invalid Mux Key or Secret\n")
color.Red("[x] Error : %s", err.Error())
return
}
if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}
color.Green("[i] Valid Mux API Key and Secret\n")
printResourcesAndPermissions(info)
}
func AnalyzePermissions(cfg *config.Config, key string, secret string) (*secretInfo, error) {
client := analyzers.NewAnalyzeClientUnrestricted(cfg)
secretInfo := &secretInfo{}
if err := testAllPermissions(client, secretInfo, key, secret); err != nil {
return nil, err
}
if err := populateAllResources(client, secretInfo, key, secret); err != nil {
return nil, err
}
return secretInfo, nil
}
func secretInfoToAnalyzerResult(info *secretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
bindings := []analyzers.Binding{}
readAccessPermission := analyzers.Permission{
Value: PermissionStrings[Read],
}
fullAccessPermission := analyzers.Permission{
Value: PermissionStrings[FullAccess],
}
videoResourcePermission := readAccessPermission
if info.hasPermission(ResourceTypeVideo, FullAccess) {
videoResourcePermission = fullAccessPermission
}
dataResourcePermission := readAccessPermission
if info.hasPermission(ResourceTypeData, FullAccess) {
dataResourcePermission = fullAccessPermission
}
systemResourcePermission := readAccessPermission
if info.hasPermission(ResourceTypeSystem, FullAccess) {
systemResourcePermission = fullAccessPermission
}
// Binding all Mux Video Assets
for _, asset := range info.Assets {
assetResource := createAssetResource(asset)
trackResources := createAssetTrackResources(asset, &assetResource)
playbackIDResources := createAssetPlaybackIDResources(asset, &assetResource)
for _, resource := range trackResources {
bindings = append(bindings, createBinding(&resource, videoResourcePermission))
}
for _, resource := range playbackIDResources {
bindings = append(bindings, createBinding(&resource, videoResourcePermission))
}
bindings = append(bindings, createBinding(&assetResource, videoResourcePermission))
}
// Binding all Mux Data Annotations
for _, annotation := range info.Annotations {
annotationResource := createAnnotationResource(annotation)
bindings = append(bindings, createBinding(&annotationResource, dataResourcePermission))
}
// Binding all Mux System Signing Keys
for _, signingKey := range info.SigningKeys {
signingKeyResource := createSigningKeyResource(signingKey)
bindings = append(bindings, createBinding(&signingKeyResource, systemResourcePermission))
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeMux,
Metadata: nil,
Bindings: bindings,
}
return &result
}
func createBinding(resource *analyzers.Resource, permission analyzers.Permission) analyzers.Binding {
return analyzers.Binding{
Resource: *resource,
Permission: permission,
}
}
func printResourcesAndPermissions(info *secretInfo) {
color.Yellow("\n[i] Permissions:")
t1 := table.NewWriter()
t1.AppendHeader(table.Row{"Resource Category", "Access Level", "Resource List"})
for idx, resource := range muxResourcesMap[ResourceTypeVideo] {
category, access := "", ""
if idx == 0 {
category = "Mux Video"
access = getAccessLevelStringFromPermission(info.Permissions[ResourceTypeVideo])
}
t1.AppendRow(table.Row{
color.GreenString(category),
color.GreenString(access),
color.GreenString(resource),
})
}
t1.AppendSeparator()
for idx, resource := range muxResourcesMap[ResourceTypeData] {
category, access := "", ""
if idx == 0 {
category = "Mux Data"
access = getAccessLevelStringFromPermission(info.Permissions[ResourceTypeData])
}
t1.AppendRow(table.Row{
color.GreenString(category),
color.GreenString(access),
color.GreenString(resource),
})
}
t1.AppendSeparator()
for idx, resource := range muxResourcesMap[ResourceTypeSystem] {
category, access := "", ""
if idx == 0 {
category = "Mux System"
access = getAccessLevelStringFromPermission(info.Permissions[ResourceTypeSystem])
}
t1.AppendRow(table.Row{
color.GreenString(category),
color.GreenString(access),
color.GreenString(resource),
})
}
t1.SetOutputMirror(os.Stdout)
t1.Render()
color.Yellow("\n[i] Resources:")
if info.hasPermission(ResourceTypeVideo, Read) {
printMuxVideoResources(info)
}
if info.hasPermission(ResourceTypeData, Read) {
printMuxDataResources(info)
}
if info.hasPermission(ResourceTypeSystem, Read) {
printMuxSystemResources(info)
}
fmt.Printf("%s: https://www.mux.com/docs/api-reference\n\n", color.GreenString("Ref"))
}
func printMuxVideoResources(info *secretInfo) {
t1 := table.NewWriter()
t1.SetTitle("Assets")
t1.AppendHeader(table.Row{"ID", "Title", "Duration", "Status", "Creator ID", "External ID", "Created At"})
t2 := table.NewWriter()
t2.SetTitle("Asset Tracks")
t2.AppendHeader(table.Row{"Asset ID", "ID", "Name", "Type", "Duration", "Status", "Primary"})
t3 := table.NewWriter()
t3.SetTitle("Asset Playback IDs")
t3.AppendHeader(table.Row{"Asset ID", "ID", "Policy"})
for _, asset := range info.Assets {
t1.AppendRow(table.Row{
color.GreenString(asset.ID),
color.GreenString(asset.Meta.Title),
color.GreenString(fmt.Sprintf("%.2fs", asset.Duration)),
color.GreenString(asset.Status),
color.GreenString(asset.Meta.CreatorID),
color.GreenString(asset.Meta.ExternalID),
color.GreenString(asset.CreatedAt),
})
t1.AppendSeparator()
for _, track := range asset.Tracks {
t2.AppendRow(table.Row{
color.GreenString(asset.ID),
color.GreenString(track.ID),
color.GreenString(track.Name),
color.GreenString(track.Type),
color.GreenString(fmt.Sprintf("%.2fs", track.Duration)),
color.GreenString(track.Status),
color.GreenString(fmt.Sprintf("%t", track.Primary)),
})
t2.AppendSeparator()
}
for _, playbackID := range asset.PlaybackIDs {
t3.AppendRow(table.Row{
color.GreenString(asset.ID),
color.GreenString(playbackID.ID),
color.GreenString(playbackID.Policy),
})
t3.AppendSeparator()
}
}
t1.SetOutputMirror(os.Stdout)
t1.Render()
t2.SetOutputMirror(os.Stdout)
t2.Render()
t3.SetOutputMirror(os.Stdout)
t3.Render()
}
func printMuxDataResources(info *secretInfo) {
t1 := table.NewWriter()
t1.SetTitle("Annotations")
t1.AppendHeader(table.Row{"ID", "Note", "Date", "Sub Property ID"})
for _, annotation := range info.Annotations {
t1.AppendRow(table.Row{
color.GreenString(annotation.ID),
color.GreenString(annotation.Note),
color.GreenString(annotation.Date),
color.GreenString(annotation.SubPropertyID),
})
}
t1.SetOutputMirror(os.Stdout)
t1.Render()
}
func printMuxSystemResources(info *secretInfo) {
t1 := table.NewWriter()
t1.SetTitle("Signing Keys")
t1.AppendHeader(table.Row{"ID", "Created At"})
for _, signingKey := range info.SigningKeys {
t1.AppendRow(table.Row{
color.GreenString(signingKey.ID),
color.GreenString(signingKey.CreatedAt),
})
}
t1.SetOutputMirror(os.Stdout)
t1.Render()
}
func getAccessLevelStringFromPermission(permission Permission) string {
switch permission {
case Read:
return "Read"
case FullAccess:
return "Read & Write"
default:
return "None"
}
}
================================================
FILE: pkg/analyzer/analyzers/mux/mux_test.go
================================================
package mux
import (
_ "embed"
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "analyzers1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
key := testSecrets.MustGetField("MUX_KEY")
secret := testSecrets.MustGetField("MUX_SECRET")
tests := []struct {
name string
key string
secret string
want string
wantErr bool
}{
{
name: "valid mux credentials",
key: key,
secret: secret,
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{
"key": tt.key,
"secret": tt.secret,
})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// compare the JSON strings
if string(gotJSON) != string(tt.want) {
// pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(tt.want, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/mux/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package mux
import "errors"
type Permission int
const (
Invalid Permission = iota
Read Permission = iota
FullAccess Permission = iota
)
var (
PermissionStrings = map[Permission]string{
Read: "read",
FullAccess: "full_access",
}
StringToPermission = map[string]Permission{
"read": Read,
"full_access": FullAccess,
}
PermissionIDs = map[Permission]int{
Read: 1,
FullAccess: 2,
}
IdToPermission = map[int]Permission{
1: Read,
2: FullAccess,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/mux/permissions.yaml
================================================
permissions:
- read
- full_access
================================================
FILE: pkg/analyzer/analyzers/mux/requests.go
================================================
package mux
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
const muxAPIBaseURL = "https://api.mux.com"
func makeAPIRequest(client *http.Client, key, secret, method, endpoint string) ([]byte, int, error) {
req, err := http.NewRequest(method, muxAPIBaseURL+"/"+endpoint, nil)
if err != nil {
return nil, 0, err
}
req.SetBasicAuth(key, secret)
res, err := client.Do(req)
if err != nil {
return nil, 0, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, 0, err
}
return body, res.StatusCode, nil
}
func testAllPermissions(client *http.Client, info *secretInfo, key string, secret string) error {
testsConfig, err := readTestsConfig()
if err != nil {
return err
}
for _, test := range testsConfig.Tests {
hasPermission, err := test.testPermission(client, key, secret)
if err != nil {
return err
}
if !hasPermission {
continue
}
info.addPermission(test.ResourceType, test.Permission)
}
return nil
}
func populateAllResources(client *http.Client, info *secretInfo, key string, secret string) error {
if info.hasPermission(ResourceTypeVideo, Read) {
if err := populateAssets(client, info, key, secret); err != nil {
return err
}
}
if info.hasPermission(ResourceTypeData, Read) {
if err := populateAnnotations(client, info, key, secret); err != nil {
return err
}
}
if info.hasPermission(ResourceTypeSystem, Read) {
if err := populateSigningKeys(client, info, key, secret); err != nil {
return err
}
}
return nil
}
func populateAssets(client *http.Client, info *secretInfo, key string, secret string) error {
const limit = 100
for page := 1; ; page++ {
url := fmt.Sprintf("/video/v1/assets?limit=%d&page=%d&timeframe[]=100:days", limit, page)
body, statusCode, err := makeAPIRequest(client, key, secret, http.MethodGet, url)
if err != nil {
return err
}
if statusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", statusCode)
}
resp := assetListResponse{}
if err := json.Unmarshal(body, &resp); err != nil {
return fmt.Errorf("failed to unmarshal data: %w", err)
}
if len(resp.Data) == 0 {
break
}
info.Assets = append(info.Assets, resp.Data...)
}
return nil
}
func populateAnnotations(client *http.Client, info *secretInfo, key string, secret string) error {
const limit = 100
for page := 1; ; page++ {
url := fmt.Sprintf("/data/v1/annotations?limit=%d&page=%d&timeframe[]=100:days", limit, page)
body, statusCode, err := makeAPIRequest(client, key, secret, http.MethodGet, url)
if err != nil {
return err
}
if statusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", statusCode)
}
resp := annotationListResponse{}
if err := json.Unmarshal(body, &resp); err != nil {
return fmt.Errorf("failed to unmarshal data: %w", err)
}
if len(resp.Data) == 0 {
break
}
info.Annotations = append(info.Annotations, resp.Data...)
}
return nil
}
func populateSigningKeys(client *http.Client, info *secretInfo, key string, secret string) error {
const limit = 100
for page := 1; ; page++ {
url := fmt.Sprintf("/system/v1/signing-keys?limit=%d&page=%d&timeframe[]=100:days", limit, page)
body, statusCode, err := makeAPIRequest(client, key, secret, http.MethodGet, url)
if err != nil {
return err
}
if statusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", statusCode)
}
resp := signingKeyListResponse{}
if err := json.Unmarshal(body, &resp); err != nil {
return fmt.Errorf("failed to unmarshal data: %w", err)
}
if len(resp.Data) == 0 {
break
}
info.SigningKeys = append(info.SigningKeys, resp.Data...)
}
return nil
}
================================================
FILE: pkg/analyzer/analyzers/mux/resources.go
================================================
package mux
import "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
var muxResourcesMap map[ResourceType][]string
func init() {
muxResourcesMap = map[ResourceType][]string{
ResourceTypeVideo: {
"Transcription Vocabularies",
"Web Inputs",
"Assets",
"Live Streams",
"Uploads",
"Playback Restrictions",
"DRM Configurations",
},
ResourceTypeData: {
"Video Views",
"Filters",
"Dimensions",
"Export",
"Metrics",
"Monitoring",
"Realtime",
"Incidents",
"Annotations",
},
ResourceTypeSystem: {
"Signing Keys",
},
}
}
func createAssetResource(asset asset) analyzers.Resource {
return analyzers.Resource{
Name: asset.ID,
FullyQualifiedName: "asset/" + asset.ID,
Type: "asset",
Metadata: map[string]any{
"duration": asset.Duration,
"status": asset.Status,
"videoQuality": asset.VideoQuality,
"mp4Support": asset.MP4Support,
"aspectRatio": asset.AspectRatio,
"createdAt": asset.CreatedAt,
},
}
}
func createAssetTrackResources(asset asset, parent *analyzers.Resource) []analyzers.Resource {
trackResources := []analyzers.Resource{}
for _, track := range asset.Tracks {
trackResources = append(trackResources, analyzers.Resource{
Name: track.ID,
FullyQualifiedName: "asset/" + asset.ID + "/track/" + track.ID,
Type: "track",
Metadata: map[string]any{
"name": track.Name,
"type": track.Type,
"duration": track.Duration,
"status": track.Status,
"primary": track.Primary,
"textType": track.TextType,
"textSource": track.TextSource,
"languageCode": track.LanguageCode,
"maxWidth": track.MaxWidth,
"maxHeight": track.MaxHeight,
"maxFrameRate": track.MaxFrameRate,
"maxChannels": track.MaxChannels,
},
Parent: parent,
})
}
return trackResources
}
func createAssetPlaybackIDResources(asset asset, parent *analyzers.Resource) []analyzers.Resource {
playbackIDResources := []analyzers.Resource{}
for _, playbackID := range asset.PlaybackIDs {
playbackIDResources = append(playbackIDResources, analyzers.Resource{
Name: playbackID.ID,
FullyQualifiedName: "asset/" + asset.ID + "/playback_id/" + playbackID.ID,
Type: "playback_id",
Metadata: map[string]any{
"policy": playbackID.Policy,
},
Parent: parent,
})
}
return playbackIDResources
}
func createAnnotationResource(annotation annotation) analyzers.Resource {
return analyzers.Resource{
Name: annotation.ID,
FullyQualifiedName: "annotation/" + annotation.ID,
Type: "annotation",
Metadata: map[string]any{
"subPropertyID": annotation.SubPropertyID,
"note": annotation.Note,
"date": annotation.Date,
},
}
}
func createSigningKeyResource(signingKey signingKey) analyzers.Resource {
return analyzers.Resource{
Name: signingKey.ID,
FullyQualifiedName: "signing_key/" + signingKey.ID,
Type: "signing_key",
Metadata: map[string]any{
"createdAt": signingKey.CreatedAt,
},
}
}
================================================
FILE: pkg/analyzer/analyzers/mux/tests.json
================================================
{
"tests": [
{
"resource_type": "video",
"permission": "read",
"endpoint": "/video/v1/assets?limit=1",
"method": "GET",
"valid_status_code": 200
},
{
"resource_type": "video",
"permission": "write",
"endpoint": "/video/v1/assets",
"method": "POST",
"valid_status_code": 400
},
{
"resource_type": "data",
"permission": "read",
"endpoint": "/data/v1/annotations?limit=1",
"method": "GET",
"valid_status_code": 200
},
{
"resource_type": "data",
"permission": "write",
"endpoint": "/data/v1/annotations",
"method": "POST",
"valid_status_code": 400
},
{
"resource_type": "system",
"permission": "read",
"endpoint": "/system/v1/signing-keys?limit=1",
"method": "GET",
"valid_status_code": 200
},
{
"resource_type": "system",
"permission": "write",
"endpoint": "/system/v1/signing-keys",
"method": "DELETE",
"valid_status_code": 400
}
]
}
================================================
FILE: pkg/analyzer/analyzers/mysql/expected_output.json
================================================
{"AnalyzerType":9,"Bindings":[{"Resource":{"Name":"ADMINISTRABLE_ROLE_AUTHORIZATIONS","FullyQualifiedName":"localhost/information_schema/ADMINISTRABLE_ROLE_AUTHORIZATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"ADMINISTRABLE_ROLE_AUTHORIZATIONS","FullyQualifiedName":"localhost/information_schema/ADMINISTRABLE_ROLE_AUTHORIZATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"ADMINISTRABLE_ROLE_AUTHORIZATIONS","FullyQualifiedName":"localhost/information_schema/ADMINISTRABLE_ROLE_AUTHORIZATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"ADMINISTRABLE_ROLE_AUTHORIZATIONS","FullyQualifiedName":"localhost/information_schema/ADMINISTRABLE_ROLE_AUTHORIZATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"ADMINISTRABLE_ROLE_AUTHORIZATIONS","FullyQualifiedName":"localhost/information_schema/ADMINISTRABLE_ROLE_AUTHORIZATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"ADMINISTRABLE_ROLE_AUTHORIZATIONS","FullyQualifiedName":"localhost/information_schema/ADMINISTRABLE_ROLE_AUTHORIZATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"ADMINISTRABLE_ROLE_AUTHORIZATIONS","FullyQualifiedName":"localhost/information_schema/ADMINISTRABLE_ROLE_AUTHORIZATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"ADMINISTRABLE_ROLE_AUTHORIZATIONS","FullyQualifiedName":"localhost/information_schema/ADMINISTRABLE_ROLE_AUTHORIZATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"ADMINISTRABLE_ROLE_AUTHORIZATIONS","FullyQualifiedName":"localhost/information_schema/ADMINISTRABLE_ROLE_AUTHORIZATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"ADMINISTRABLE_ROLE_AUTHORIZATIONS","FullyQualifiedName":"localhost/information_schema/ADMINISTRABLE_ROLE_AUTHORIZATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"ADMINISTRABLE_ROLE_AUTHORIZATIONS","FullyQualifiedName":"localhost/information_schema/ADMINISTRABLE_ROLE_AUTHORIZATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"ADMINISTRABLE_ROLE_AUTHORIZATIONS","FullyQualifiedName":"localhost/information_schema/ADMINISTRABLE_ROLE_AUTHORIZATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"APPLICABLE_ROLES","FullyQualifiedName":"localhost/information_schema/APPLICABLE_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"APPLICABLE_ROLES","FullyQualifiedName":"localhost/information_schema/APPLICABLE_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"APPLICABLE_ROLES","FullyQualifiedName":"localhost/information_schema/APPLICABLE_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"APPLICABLE_ROLES","FullyQualifiedName":"localhost/information_schema/APPLICABLE_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"APPLICABLE_ROLES","FullyQualifiedName":"localhost/information_schema/APPLICABLE_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"APPLICABLE_ROLES","FullyQualifiedName":"localhost/information_schema/APPLICABLE_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"APPLICABLE_ROLES","FullyQualifiedName":"localhost/information_schema/APPLICABLE_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"APPLICABLE_ROLES","FullyQualifiedName":"localhost/information_schema/APPLICABLE_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"APPLICABLE_ROLES","FullyQualifiedName":"localhost/information_schema/APPLICABLE_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"APPLICABLE_ROLES","FullyQualifiedName":"localhost/information_schema/APPLICABLE_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"APPLICABLE_ROLES","FullyQualifiedName":"localhost/information_schema/APPLICABLE_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"APPLICABLE_ROLES","FullyQualifiedName":"localhost/information_schema/APPLICABLE_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"CHARACTER_SETS","FullyQualifiedName":"localhost/information_schema/CHARACTER_SETS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"CHARACTER_SETS","FullyQualifiedName":"localhost/information_schema/CHARACTER_SETS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"CHARACTER_SETS","FullyQualifiedName":"localhost/information_schema/CHARACTER_SETS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"CHARACTER_SETS","FullyQualifiedName":"localhost/information_schema/CHARACTER_SETS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"CHARACTER_SETS","FullyQualifiedName":"localhost/information_schema/CHARACTER_SETS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"CHARACTER_SETS","FullyQualifiedName":"localhost/information_schema/CHARACTER_SETS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"CHARACTER_SETS","FullyQualifiedName":"localhost/information_schema/CHARACTER_SETS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"CHARACTER_SETS","FullyQualifiedName":"localhost/information_schema/CHARACTER_SETS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"CHARACTER_SETS","FullyQualifiedName":"localhost/information_schema/CHARACTER_SETS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"CHARACTER_SETS","FullyQualifiedName":"localhost/information_schema/CHARACTER_SETS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"CHARACTER_SETS","FullyQualifiedName":"localhost/information_schema/CHARACTER_SETS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"CHARACTER_SETS","FullyQualifiedName":"localhost/information_schema/CHARACTER_SETS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"CHECK_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/CHECK_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"CHECK_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/CHECK_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"CHECK_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/CHECK_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"CHECK_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/CHECK_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"CHECK_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/CHECK_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"CHECK_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/CHECK_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"CHECK_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/CHECK_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"CHECK_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/CHECK_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"CHECK_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/CHECK_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"CHECK_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/CHECK_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"CHECK_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/CHECK_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"CHECK_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/CHECK_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"COLLATIONS","FullyQualifiedName":"localhost/information_schema/COLLATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"COLLATIONS","FullyQualifiedName":"localhost/information_schema/COLLATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"COLLATIONS","FullyQualifiedName":"localhost/information_schema/COLLATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"COLLATIONS","FullyQualifiedName":"localhost/information_schema/COLLATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"COLLATIONS","FullyQualifiedName":"localhost/information_schema/COLLATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"COLLATIONS","FullyQualifiedName":"localhost/information_schema/COLLATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"COLLATIONS","FullyQualifiedName":"localhost/information_schema/COLLATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"COLLATIONS","FullyQualifiedName":"localhost/information_schema/COLLATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"COLLATIONS","FullyQualifiedName":"localhost/information_schema/COLLATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"COLLATIONS","FullyQualifiedName":"localhost/information_schema/COLLATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"COLLATIONS","FullyQualifiedName":"localhost/information_schema/COLLATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"COLLATIONS","FullyQualifiedName":"localhost/information_schema/COLLATIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"COLLATION_CHARACTER_SET_APPLICABILITY","FullyQualifiedName":"localhost/information_schema/COLLATION_CHARACTER_SET_APPLICABILITY","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"COLLATION_CHARACTER_SET_APPLICABILITY","FullyQualifiedName":"localhost/information_schema/COLLATION_CHARACTER_SET_APPLICABILITY","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"COLLATION_CHARACTER_SET_APPLICABILITY","FullyQualifiedName":"localhost/information_schema/COLLATION_CHARACTER_SET_APPLICABILITY","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"COLLATION_CHARACTER_SET_APPLICABILITY","FullyQualifiedName":"localhost/information_schema/COLLATION_CHARACTER_SET_APPLICABILITY","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"COLLATION_CHARACTER_SET_APPLICABILITY","FullyQualifiedName":"localhost/information_schema/COLLATION_CHARACTER_SET_APPLICABILITY","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"COLLATION_CHARACTER_SET_APPLICABILITY","FullyQualifiedName":"localhost/information_schema/COLLATION_CHARACTER_SET_APPLICABILITY","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"COLLATION_CHARACTER_SET_APPLICABILITY","FullyQualifiedName":"localhost/information_schema/COLLATION_CHARACTER_SET_APPLICABILITY","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"COLLATION_CHARACTER_SET_APPLICABILITY","FullyQualifiedName":"localhost/information_schema/COLLATION_CHARACTER_SET_APPLICABILITY","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"COLLATION_CHARACTER_SET_APPLICABILITY","FullyQualifiedName":"localhost/information_schema/COLLATION_CHARACTER_SET_APPLICABILITY","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"COLLATION_CHARACTER_SET_APPLICABILITY","FullyQualifiedName":"localhost/information_schema/COLLATION_CHARACTER_SET_APPLICABILITY","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"COLLATION_CHARACTER_SET_APPLICABILITY","FullyQualifiedName":"localhost/information_schema/COLLATION_CHARACTER_SET_APPLICABILITY","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"COLLATION_CHARACTER_SET_APPLICABILITY","FullyQualifiedName":"localhost/information_schema/COLLATION_CHARACTER_SET_APPLICABILITY","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"COLUMNS","FullyQualifiedName":"localhost/information_schema/COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"COLUMNS","FullyQualifiedName":"localhost/information_schema/COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"COLUMNS","FullyQualifiedName":"localhost/information_schema/COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"COLUMNS","FullyQualifiedName":"localhost/information_schema/COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"COLUMNS","FullyQualifiedName":"localhost/information_schema/COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"COLUMNS","FullyQualifiedName":"localhost/information_schema/COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"COLUMNS","FullyQualifiedName":"localhost/information_schema/COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"COLUMNS","FullyQualifiedName":"localhost/information_schema/COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"COLUMNS","FullyQualifiedName":"localhost/information_schema/COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"COLUMNS","FullyQualifiedName":"localhost/information_schema/COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"COLUMNS","FullyQualifiedName":"localhost/information_schema/COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"COLUMNS","FullyQualifiedName":"localhost/information_schema/COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"COLUMNS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/COLUMNS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"COLUMNS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/COLUMNS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"COLUMNS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/COLUMNS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"COLUMNS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/COLUMNS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"COLUMNS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/COLUMNS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"COLUMNS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/COLUMNS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"COLUMNS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/COLUMNS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"COLUMNS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/COLUMNS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"COLUMNS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/COLUMNS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"COLUMNS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/COLUMNS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"COLUMNS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/COLUMNS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"COLUMNS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/COLUMNS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"COLUMN_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/COLUMN_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"COLUMN_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/COLUMN_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"COLUMN_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/COLUMN_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"COLUMN_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/COLUMN_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"COLUMN_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/COLUMN_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"COLUMN_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/COLUMN_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"COLUMN_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/COLUMN_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"COLUMN_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/COLUMN_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"COLUMN_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/COLUMN_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"COLUMN_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/COLUMN_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"COLUMN_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/COLUMN_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"COLUMN_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/COLUMN_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"COLUMN_STATISTICS","FullyQualifiedName":"localhost/information_schema/COLUMN_STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"COLUMN_STATISTICS","FullyQualifiedName":"localhost/information_schema/COLUMN_STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"COLUMN_STATISTICS","FullyQualifiedName":"localhost/information_schema/COLUMN_STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"COLUMN_STATISTICS","FullyQualifiedName":"localhost/information_schema/COLUMN_STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"COLUMN_STATISTICS","FullyQualifiedName":"localhost/information_schema/COLUMN_STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"COLUMN_STATISTICS","FullyQualifiedName":"localhost/information_schema/COLUMN_STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"COLUMN_STATISTICS","FullyQualifiedName":"localhost/information_schema/COLUMN_STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"COLUMN_STATISTICS","FullyQualifiedName":"localhost/information_schema/COLUMN_STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"COLUMN_STATISTICS","FullyQualifiedName":"localhost/information_schema/COLUMN_STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"COLUMN_STATISTICS","FullyQualifiedName":"localhost/information_schema/COLUMN_STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"COLUMN_STATISTICS","FullyQualifiedName":"localhost/information_schema/COLUMN_STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"COLUMN_STATISTICS","FullyQualifiedName":"localhost/information_schema/COLUMN_STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"ENABLED_ROLES","FullyQualifiedName":"localhost/information_schema/ENABLED_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"ENABLED_ROLES","FullyQualifiedName":"localhost/information_schema/ENABLED_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"ENABLED_ROLES","FullyQualifiedName":"localhost/information_schema/ENABLED_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"ENABLED_ROLES","FullyQualifiedName":"localhost/information_schema/ENABLED_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"ENABLED_ROLES","FullyQualifiedName":"localhost/information_schema/ENABLED_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"ENABLED_ROLES","FullyQualifiedName":"localhost/information_schema/ENABLED_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"ENABLED_ROLES","FullyQualifiedName":"localhost/information_schema/ENABLED_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"ENABLED_ROLES","FullyQualifiedName":"localhost/information_schema/ENABLED_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"ENABLED_ROLES","FullyQualifiedName":"localhost/information_schema/ENABLED_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"ENABLED_ROLES","FullyQualifiedName":"localhost/information_schema/ENABLED_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"ENABLED_ROLES","FullyQualifiedName":"localhost/information_schema/ENABLED_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"ENABLED_ROLES","FullyQualifiedName":"localhost/information_schema/ENABLED_ROLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"ENGINES","FullyQualifiedName":"localhost/information_schema/ENGINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"ENGINES","FullyQualifiedName":"localhost/information_schema/ENGINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"ENGINES","FullyQualifiedName":"localhost/information_schema/ENGINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"ENGINES","FullyQualifiedName":"localhost/information_schema/ENGINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"ENGINES","FullyQualifiedName":"localhost/information_schema/ENGINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"ENGINES","FullyQualifiedName":"localhost/information_schema/ENGINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"ENGINES","FullyQualifiedName":"localhost/information_schema/ENGINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"ENGINES","FullyQualifiedName":"localhost/information_schema/ENGINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"ENGINES","FullyQualifiedName":"localhost/information_schema/ENGINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"ENGINES","FullyQualifiedName":"localhost/information_schema/ENGINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"ENGINES","FullyQualifiedName":"localhost/information_schema/ENGINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"ENGINES","FullyQualifiedName":"localhost/information_schema/ENGINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"EVENTS","FullyQualifiedName":"localhost/information_schema/EVENTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"EVENTS","FullyQualifiedName":"localhost/information_schema/EVENTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"EVENTS","FullyQualifiedName":"localhost/information_schema/EVENTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"EVENTS","FullyQualifiedName":"localhost/information_schema/EVENTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"EVENTS","FullyQualifiedName":"localhost/information_schema/EVENTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"EVENTS","FullyQualifiedName":"localhost/information_schema/EVENTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"EVENTS","FullyQualifiedName":"localhost/information_schema/EVENTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"EVENTS","FullyQualifiedName":"localhost/information_schema/EVENTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"EVENTS","FullyQualifiedName":"localhost/information_schema/EVENTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"EVENTS","FullyQualifiedName":"localhost/information_schema/EVENTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"EVENTS","FullyQualifiedName":"localhost/information_schema/EVENTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"EVENTS","FullyQualifiedName":"localhost/information_schema/EVENTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"FILES","FullyQualifiedName":"localhost/information_schema/FILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"FILES","FullyQualifiedName":"localhost/information_schema/FILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"FILES","FullyQualifiedName":"localhost/information_schema/FILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"FILES","FullyQualifiedName":"localhost/information_schema/FILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"FILES","FullyQualifiedName":"localhost/information_schema/FILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"FILES","FullyQualifiedName":"localhost/information_schema/FILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"FILES","FullyQualifiedName":"localhost/information_schema/FILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"FILES","FullyQualifiedName":"localhost/information_schema/FILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"FILES","FullyQualifiedName":"localhost/information_schema/FILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"FILES","FullyQualifiedName":"localhost/information_schema/FILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"FILES","FullyQualifiedName":"localhost/information_schema/FILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"FILES","FullyQualifiedName":"localhost/information_schema/FILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE_LRU","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE_LRU","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE_LRU","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE_LRU","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE_LRU","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE_LRU","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE_LRU","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE_LRU","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE_LRU","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE_LRU","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE_LRU","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE_LRU","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE_LRU","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE_LRU","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE_LRU","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE_LRU","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE_LRU","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE_LRU","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE_LRU","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE_LRU","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE_LRU","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE_LRU","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_PAGE_LRU","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_PAGE_LRU","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_POOL_STATS","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_POOL_STATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_POOL_STATS","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_POOL_STATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_POOL_STATS","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_POOL_STATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_POOL_STATS","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_POOL_STATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_POOL_STATS","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_POOL_STATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_POOL_STATS","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_POOL_STATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_POOL_STATS","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_POOL_STATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_POOL_STATS","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_POOL_STATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_POOL_STATS","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_POOL_STATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_POOL_STATS","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_POOL_STATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_POOL_STATS","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_POOL_STATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_BUFFER_POOL_STATS","FullyQualifiedName":"localhost/information_schema/INNODB_BUFFER_POOL_STATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_CACHED_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_CACHED_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_CACHED_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_CACHED_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_CACHED_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_CACHED_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_CACHED_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_CACHED_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_CACHED_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_CACHED_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_CACHED_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_CACHED_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_CACHED_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_CACHED_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_CACHED_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_CACHED_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_CACHED_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_CACHED_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_CACHED_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_CACHED_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_CACHED_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_CACHED_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_CACHED_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_CACHED_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_CMP","FullyQualifiedName":"localhost/information_schema/INNODB_CMP","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_CMP","FullyQualifiedName":"localhost/information_schema/INNODB_CMP","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_CMP","FullyQualifiedName":"localhost/information_schema/INNODB_CMP","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_CMP","FullyQualifiedName":"localhost/information_schema/INNODB_CMP","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_CMP","FullyQualifiedName":"localhost/information_schema/INNODB_CMP","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_CMP","FullyQualifiedName":"localhost/information_schema/INNODB_CMP","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_CMP","FullyQualifiedName":"localhost/information_schema/INNODB_CMP","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_CMP","FullyQualifiedName":"localhost/information_schema/INNODB_CMP","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_CMP","FullyQualifiedName":"localhost/information_schema/INNODB_CMP","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_CMP","FullyQualifiedName":"localhost/information_schema/INNODB_CMP","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_CMP","FullyQualifiedName":"localhost/information_schema/INNODB_CMP","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_CMP","FullyQualifiedName":"localhost/information_schema/INNODB_CMP","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_CMPMEM_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMPMEM_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_CMP_PER_INDEX_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_PER_INDEX_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_CMP_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_CMP_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_CMP_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_CMP_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_CMP_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_CMP_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_CMP_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_CMP_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_CMP_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_CMP_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_CMP_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_CMP_RESET","FullyQualifiedName":"localhost/information_schema/INNODB_CMP_RESET","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_COLUMNS","FullyQualifiedName":"localhost/information_schema/INNODB_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_COLUMNS","FullyQualifiedName":"localhost/information_schema/INNODB_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_COLUMNS","FullyQualifiedName":"localhost/information_schema/INNODB_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_COLUMNS","FullyQualifiedName":"localhost/information_schema/INNODB_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_COLUMNS","FullyQualifiedName":"localhost/information_schema/INNODB_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_COLUMNS","FullyQualifiedName":"localhost/information_schema/INNODB_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_COLUMNS","FullyQualifiedName":"localhost/information_schema/INNODB_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_COLUMNS","FullyQualifiedName":"localhost/information_schema/INNODB_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_COLUMNS","FullyQualifiedName":"localhost/information_schema/INNODB_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_COLUMNS","FullyQualifiedName":"localhost/information_schema/INNODB_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_COLUMNS","FullyQualifiedName":"localhost/information_schema/INNODB_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_COLUMNS","FullyQualifiedName":"localhost/information_schema/INNODB_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_DATAFILES","FullyQualifiedName":"localhost/information_schema/INNODB_DATAFILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_DATAFILES","FullyQualifiedName":"localhost/information_schema/INNODB_DATAFILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_DATAFILES","FullyQualifiedName":"localhost/information_schema/INNODB_DATAFILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_DATAFILES","FullyQualifiedName":"localhost/information_schema/INNODB_DATAFILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_DATAFILES","FullyQualifiedName":"localhost/information_schema/INNODB_DATAFILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_DATAFILES","FullyQualifiedName":"localhost/information_schema/INNODB_DATAFILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_DATAFILES","FullyQualifiedName":"localhost/information_schema/INNODB_DATAFILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_DATAFILES","FullyQualifiedName":"localhost/information_schema/INNODB_DATAFILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_DATAFILES","FullyQualifiedName":"localhost/information_schema/INNODB_DATAFILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_DATAFILES","FullyQualifiedName":"localhost/information_schema/INNODB_DATAFILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_DATAFILES","FullyQualifiedName":"localhost/information_schema/INNODB_DATAFILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_DATAFILES","FullyQualifiedName":"localhost/information_schema/INNODB_DATAFILES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_FIELDS","FullyQualifiedName":"localhost/information_schema/INNODB_FIELDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_FIELDS","FullyQualifiedName":"localhost/information_schema/INNODB_FIELDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_FIELDS","FullyQualifiedName":"localhost/information_schema/INNODB_FIELDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FIELDS","FullyQualifiedName":"localhost/information_schema/INNODB_FIELDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_FIELDS","FullyQualifiedName":"localhost/information_schema/INNODB_FIELDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_FIELDS","FullyQualifiedName":"localhost/information_schema/INNODB_FIELDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_FIELDS","FullyQualifiedName":"localhost/information_schema/INNODB_FIELDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_FIELDS","FullyQualifiedName":"localhost/information_schema/INNODB_FIELDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_FIELDS","FullyQualifiedName":"localhost/information_schema/INNODB_FIELDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_FIELDS","FullyQualifiedName":"localhost/information_schema/INNODB_FIELDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FIELDS","FullyQualifiedName":"localhost/information_schema/INNODB_FIELDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_FIELDS","FullyQualifiedName":"localhost/information_schema/INNODB_FIELDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN_COLS","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN_COLS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN_COLS","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN_COLS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN_COLS","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN_COLS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN_COLS","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN_COLS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN_COLS","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN_COLS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN_COLS","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN_COLS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN_COLS","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN_COLS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN_COLS","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN_COLS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN_COLS","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN_COLS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN_COLS","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN_COLS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN_COLS","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN_COLS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_FOREIGN_COLS","FullyQualifiedName":"localhost/information_schema/INNODB_FOREIGN_COLS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_FT_BEING_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_BEING_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_FT_BEING_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_BEING_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_FT_BEING_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_BEING_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FT_BEING_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_BEING_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_FT_BEING_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_BEING_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_FT_BEING_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_BEING_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_FT_BEING_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_BEING_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_FT_BEING_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_BEING_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_FT_BEING_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_BEING_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_FT_BEING_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_BEING_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FT_BEING_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_BEING_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_FT_BEING_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_BEING_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_FT_CONFIG","FullyQualifiedName":"localhost/information_schema/INNODB_FT_CONFIG","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_FT_CONFIG","FullyQualifiedName":"localhost/information_schema/INNODB_FT_CONFIG","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_FT_CONFIG","FullyQualifiedName":"localhost/information_schema/INNODB_FT_CONFIG","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FT_CONFIG","FullyQualifiedName":"localhost/information_schema/INNODB_FT_CONFIG","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_FT_CONFIG","FullyQualifiedName":"localhost/information_schema/INNODB_FT_CONFIG","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_FT_CONFIG","FullyQualifiedName":"localhost/information_schema/INNODB_FT_CONFIG","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_FT_CONFIG","FullyQualifiedName":"localhost/information_schema/INNODB_FT_CONFIG","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_FT_CONFIG","FullyQualifiedName":"localhost/information_schema/INNODB_FT_CONFIG","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_FT_CONFIG","FullyQualifiedName":"localhost/information_schema/INNODB_FT_CONFIG","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_FT_CONFIG","FullyQualifiedName":"localhost/information_schema/INNODB_FT_CONFIG","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FT_CONFIG","FullyQualifiedName":"localhost/information_schema/INNODB_FT_CONFIG","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_FT_CONFIG","FullyQualifiedName":"localhost/information_schema/INNODB_FT_CONFIG","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_FT_DEFAULT_STOPWORD","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DEFAULT_STOPWORD","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_FT_DEFAULT_STOPWORD","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DEFAULT_STOPWORD","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_FT_DEFAULT_STOPWORD","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DEFAULT_STOPWORD","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FT_DEFAULT_STOPWORD","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DEFAULT_STOPWORD","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_FT_DEFAULT_STOPWORD","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DEFAULT_STOPWORD","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_FT_DEFAULT_STOPWORD","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DEFAULT_STOPWORD","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_FT_DEFAULT_STOPWORD","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DEFAULT_STOPWORD","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_FT_DEFAULT_STOPWORD","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DEFAULT_STOPWORD","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_FT_DEFAULT_STOPWORD","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DEFAULT_STOPWORD","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_FT_DEFAULT_STOPWORD","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DEFAULT_STOPWORD","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FT_DEFAULT_STOPWORD","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DEFAULT_STOPWORD","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_FT_DEFAULT_STOPWORD","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DEFAULT_STOPWORD","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_FT_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_FT_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_FT_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FT_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_FT_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_FT_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_FT_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_FT_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_FT_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_FT_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FT_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_FT_DELETED","FullyQualifiedName":"localhost/information_schema/INNODB_FT_DELETED","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_CACHE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_CACHE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_CACHE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_CACHE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_CACHE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_CACHE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_CACHE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_CACHE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_CACHE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_CACHE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_CACHE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_CACHE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_CACHE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_CACHE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_CACHE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_CACHE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_CACHE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_CACHE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_CACHE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_CACHE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_CACHE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_CACHE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_CACHE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_CACHE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_TABLE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_TABLE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_TABLE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_TABLE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_TABLE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_TABLE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_TABLE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_TABLE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_TABLE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_TABLE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_TABLE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_TABLE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_TABLE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_TABLE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_TABLE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_TABLE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_TABLE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_TABLE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_TABLE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_TABLE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_TABLE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_TABLE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_FT_INDEX_TABLE","FullyQualifiedName":"localhost/information_schema/INNODB_FT_INDEX_TABLE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_INDEXES","FullyQualifiedName":"localhost/information_schema/INNODB_INDEXES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_METRICS","FullyQualifiedName":"localhost/information_schema/INNODB_METRICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_METRICS","FullyQualifiedName":"localhost/information_schema/INNODB_METRICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_METRICS","FullyQualifiedName":"localhost/information_schema/INNODB_METRICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_METRICS","FullyQualifiedName":"localhost/information_schema/INNODB_METRICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_METRICS","FullyQualifiedName":"localhost/information_schema/INNODB_METRICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_METRICS","FullyQualifiedName":"localhost/information_schema/INNODB_METRICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_METRICS","FullyQualifiedName":"localhost/information_schema/INNODB_METRICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_METRICS","FullyQualifiedName":"localhost/information_schema/INNODB_METRICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_METRICS","FullyQualifiedName":"localhost/information_schema/INNODB_METRICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_METRICS","FullyQualifiedName":"localhost/information_schema/INNODB_METRICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_METRICS","FullyQualifiedName":"localhost/information_schema/INNODB_METRICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_METRICS","FullyQualifiedName":"localhost/information_schema/INNODB_METRICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_SESSION_TEMP_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_SESSION_TEMP_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_SESSION_TEMP_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_SESSION_TEMP_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_SESSION_TEMP_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_SESSION_TEMP_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_SESSION_TEMP_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_SESSION_TEMP_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_SESSION_TEMP_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_SESSION_TEMP_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_SESSION_TEMP_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_SESSION_TEMP_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_SESSION_TEMP_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_SESSION_TEMP_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_SESSION_TEMP_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_SESSION_TEMP_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_SESSION_TEMP_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_SESSION_TEMP_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_SESSION_TEMP_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_SESSION_TEMP_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_SESSION_TEMP_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_SESSION_TEMP_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_SESSION_TEMP_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_SESSION_TEMP_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_TABLES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_TABLES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_TABLES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_TABLES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_TABLES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_TABLES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_TABLES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_TABLES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_TABLES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_TABLES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_TABLES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_TABLES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES_BRIEF","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES_BRIEF","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES_BRIEF","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES_BRIEF","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES_BRIEF","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES_BRIEF","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES_BRIEF","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES_BRIEF","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES_BRIEF","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES_BRIEF","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES_BRIEF","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES_BRIEF","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES_BRIEF","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES_BRIEF","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES_BRIEF","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES_BRIEF","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES_BRIEF","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES_BRIEF","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES_BRIEF","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES_BRIEF","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES_BRIEF","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES_BRIEF","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_TABLESPACES_BRIEF","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESPACES_BRIEF","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_TABLESTATS","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESTATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_TABLESTATS","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESTATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_TABLESTATS","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESTATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_TABLESTATS","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESTATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_TABLESTATS","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESTATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_TABLESTATS","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESTATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_TABLESTATS","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESTATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_TABLESTATS","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESTATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_TABLESTATS","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESTATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_TABLESTATS","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESTATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_TABLESTATS","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESTATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_TABLESTATS","FullyQualifiedName":"localhost/information_schema/INNODB_TABLESTATS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_TEMP_TABLE_INFO","FullyQualifiedName":"localhost/information_schema/INNODB_TEMP_TABLE_INFO","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_TEMP_TABLE_INFO","FullyQualifiedName":"localhost/information_schema/INNODB_TEMP_TABLE_INFO","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_TEMP_TABLE_INFO","FullyQualifiedName":"localhost/information_schema/INNODB_TEMP_TABLE_INFO","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_TEMP_TABLE_INFO","FullyQualifiedName":"localhost/information_schema/INNODB_TEMP_TABLE_INFO","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_TEMP_TABLE_INFO","FullyQualifiedName":"localhost/information_schema/INNODB_TEMP_TABLE_INFO","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_TEMP_TABLE_INFO","FullyQualifiedName":"localhost/information_schema/INNODB_TEMP_TABLE_INFO","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_TEMP_TABLE_INFO","FullyQualifiedName":"localhost/information_schema/INNODB_TEMP_TABLE_INFO","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_TEMP_TABLE_INFO","FullyQualifiedName":"localhost/information_schema/INNODB_TEMP_TABLE_INFO","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_TEMP_TABLE_INFO","FullyQualifiedName":"localhost/information_schema/INNODB_TEMP_TABLE_INFO","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_TEMP_TABLE_INFO","FullyQualifiedName":"localhost/information_schema/INNODB_TEMP_TABLE_INFO","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_TEMP_TABLE_INFO","FullyQualifiedName":"localhost/information_schema/INNODB_TEMP_TABLE_INFO","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_TEMP_TABLE_INFO","FullyQualifiedName":"localhost/information_schema/INNODB_TEMP_TABLE_INFO","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_TRX","FullyQualifiedName":"localhost/information_schema/INNODB_TRX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_TRX","FullyQualifiedName":"localhost/information_schema/INNODB_TRX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_TRX","FullyQualifiedName":"localhost/information_schema/INNODB_TRX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_TRX","FullyQualifiedName":"localhost/information_schema/INNODB_TRX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_TRX","FullyQualifiedName":"localhost/information_schema/INNODB_TRX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_TRX","FullyQualifiedName":"localhost/information_schema/INNODB_TRX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_TRX","FullyQualifiedName":"localhost/information_schema/INNODB_TRX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_TRX","FullyQualifiedName":"localhost/information_schema/INNODB_TRX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_TRX","FullyQualifiedName":"localhost/information_schema/INNODB_TRX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_TRX","FullyQualifiedName":"localhost/information_schema/INNODB_TRX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_TRX","FullyQualifiedName":"localhost/information_schema/INNODB_TRX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_TRX","FullyQualifiedName":"localhost/information_schema/INNODB_TRX","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"INNODB_VIRTUAL","FullyQualifiedName":"localhost/information_schema/INNODB_VIRTUAL","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"INNODB_VIRTUAL","FullyQualifiedName":"localhost/information_schema/INNODB_VIRTUAL","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"INNODB_VIRTUAL","FullyQualifiedName":"localhost/information_schema/INNODB_VIRTUAL","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"INNODB_VIRTUAL","FullyQualifiedName":"localhost/information_schema/INNODB_VIRTUAL","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"INNODB_VIRTUAL","FullyQualifiedName":"localhost/information_schema/INNODB_VIRTUAL","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"INNODB_VIRTUAL","FullyQualifiedName":"localhost/information_schema/INNODB_VIRTUAL","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"INNODB_VIRTUAL","FullyQualifiedName":"localhost/information_schema/INNODB_VIRTUAL","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"INNODB_VIRTUAL","FullyQualifiedName":"localhost/information_schema/INNODB_VIRTUAL","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"INNODB_VIRTUAL","FullyQualifiedName":"localhost/information_schema/INNODB_VIRTUAL","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"INNODB_VIRTUAL","FullyQualifiedName":"localhost/information_schema/INNODB_VIRTUAL","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"INNODB_VIRTUAL","FullyQualifiedName":"localhost/information_schema/INNODB_VIRTUAL","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"INNODB_VIRTUAL","FullyQualifiedName":"localhost/information_schema/INNODB_VIRTUAL","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"KEYWORDS","FullyQualifiedName":"localhost/information_schema/KEYWORDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"KEYWORDS","FullyQualifiedName":"localhost/information_schema/KEYWORDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"KEYWORDS","FullyQualifiedName":"localhost/information_schema/KEYWORDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"KEYWORDS","FullyQualifiedName":"localhost/information_schema/KEYWORDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"KEYWORDS","FullyQualifiedName":"localhost/information_schema/KEYWORDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"KEYWORDS","FullyQualifiedName":"localhost/information_schema/KEYWORDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"KEYWORDS","FullyQualifiedName":"localhost/information_schema/KEYWORDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"KEYWORDS","FullyQualifiedName":"localhost/information_schema/KEYWORDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"KEYWORDS","FullyQualifiedName":"localhost/information_schema/KEYWORDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"KEYWORDS","FullyQualifiedName":"localhost/information_schema/KEYWORDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"KEYWORDS","FullyQualifiedName":"localhost/information_schema/KEYWORDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"KEYWORDS","FullyQualifiedName":"localhost/information_schema/KEYWORDS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"KEY_COLUMN_USAGE","FullyQualifiedName":"localhost/information_schema/KEY_COLUMN_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"KEY_COLUMN_USAGE","FullyQualifiedName":"localhost/information_schema/KEY_COLUMN_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"KEY_COLUMN_USAGE","FullyQualifiedName":"localhost/information_schema/KEY_COLUMN_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"KEY_COLUMN_USAGE","FullyQualifiedName":"localhost/information_schema/KEY_COLUMN_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"KEY_COLUMN_USAGE","FullyQualifiedName":"localhost/information_schema/KEY_COLUMN_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"KEY_COLUMN_USAGE","FullyQualifiedName":"localhost/information_schema/KEY_COLUMN_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"KEY_COLUMN_USAGE","FullyQualifiedName":"localhost/information_schema/KEY_COLUMN_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"KEY_COLUMN_USAGE","FullyQualifiedName":"localhost/information_schema/KEY_COLUMN_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"KEY_COLUMN_USAGE","FullyQualifiedName":"localhost/information_schema/KEY_COLUMN_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"KEY_COLUMN_USAGE","FullyQualifiedName":"localhost/information_schema/KEY_COLUMN_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"KEY_COLUMN_USAGE","FullyQualifiedName":"localhost/information_schema/KEY_COLUMN_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"KEY_COLUMN_USAGE","FullyQualifiedName":"localhost/information_schema/KEY_COLUMN_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"OPTIMIZER_TRACE","FullyQualifiedName":"localhost/information_schema/OPTIMIZER_TRACE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"OPTIMIZER_TRACE","FullyQualifiedName":"localhost/information_schema/OPTIMIZER_TRACE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"OPTIMIZER_TRACE","FullyQualifiedName":"localhost/information_schema/OPTIMIZER_TRACE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"OPTIMIZER_TRACE","FullyQualifiedName":"localhost/information_schema/OPTIMIZER_TRACE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"OPTIMIZER_TRACE","FullyQualifiedName":"localhost/information_schema/OPTIMIZER_TRACE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"OPTIMIZER_TRACE","FullyQualifiedName":"localhost/information_schema/OPTIMIZER_TRACE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"OPTIMIZER_TRACE","FullyQualifiedName":"localhost/information_schema/OPTIMIZER_TRACE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"OPTIMIZER_TRACE","FullyQualifiedName":"localhost/information_schema/OPTIMIZER_TRACE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"OPTIMIZER_TRACE","FullyQualifiedName":"localhost/information_schema/OPTIMIZER_TRACE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"OPTIMIZER_TRACE","FullyQualifiedName":"localhost/information_schema/OPTIMIZER_TRACE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"OPTIMIZER_TRACE","FullyQualifiedName":"localhost/information_schema/OPTIMIZER_TRACE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"OPTIMIZER_TRACE","FullyQualifiedName":"localhost/information_schema/OPTIMIZER_TRACE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"PARAMETERS","FullyQualifiedName":"localhost/information_schema/PARAMETERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"PARAMETERS","FullyQualifiedName":"localhost/information_schema/PARAMETERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"PARAMETERS","FullyQualifiedName":"localhost/information_schema/PARAMETERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"PARAMETERS","FullyQualifiedName":"localhost/information_schema/PARAMETERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"PARAMETERS","FullyQualifiedName":"localhost/information_schema/PARAMETERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"PARAMETERS","FullyQualifiedName":"localhost/information_schema/PARAMETERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"PARAMETERS","FullyQualifiedName":"localhost/information_schema/PARAMETERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"PARAMETERS","FullyQualifiedName":"localhost/information_schema/PARAMETERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"PARAMETERS","FullyQualifiedName":"localhost/information_schema/PARAMETERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"PARAMETERS","FullyQualifiedName":"localhost/information_schema/PARAMETERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"PARAMETERS","FullyQualifiedName":"localhost/information_schema/PARAMETERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"PARAMETERS","FullyQualifiedName":"localhost/information_schema/PARAMETERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"PARTITIONS","FullyQualifiedName":"localhost/information_schema/PARTITIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"PARTITIONS","FullyQualifiedName":"localhost/information_schema/PARTITIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"PARTITIONS","FullyQualifiedName":"localhost/information_schema/PARTITIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"PARTITIONS","FullyQualifiedName":"localhost/information_schema/PARTITIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"PARTITIONS","FullyQualifiedName":"localhost/information_schema/PARTITIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"PARTITIONS","FullyQualifiedName":"localhost/information_schema/PARTITIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"PARTITIONS","FullyQualifiedName":"localhost/information_schema/PARTITIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"PARTITIONS","FullyQualifiedName":"localhost/information_schema/PARTITIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"PARTITIONS","FullyQualifiedName":"localhost/information_schema/PARTITIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"PARTITIONS","FullyQualifiedName":"localhost/information_schema/PARTITIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"PARTITIONS","FullyQualifiedName":"localhost/information_schema/PARTITIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"PARTITIONS","FullyQualifiedName":"localhost/information_schema/PARTITIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"PLUGINS","FullyQualifiedName":"localhost/information_schema/PLUGINS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"PLUGINS","FullyQualifiedName":"localhost/information_schema/PLUGINS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"PLUGINS","FullyQualifiedName":"localhost/information_schema/PLUGINS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"PLUGINS","FullyQualifiedName":"localhost/information_schema/PLUGINS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"PLUGINS","FullyQualifiedName":"localhost/information_schema/PLUGINS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"PLUGINS","FullyQualifiedName":"localhost/information_schema/PLUGINS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"PLUGINS","FullyQualifiedName":"localhost/information_schema/PLUGINS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"PLUGINS","FullyQualifiedName":"localhost/information_schema/PLUGINS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"PLUGINS","FullyQualifiedName":"localhost/information_schema/PLUGINS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"PLUGINS","FullyQualifiedName":"localhost/information_schema/PLUGINS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"PLUGINS","FullyQualifiedName":"localhost/information_schema/PLUGINS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"PLUGINS","FullyQualifiedName":"localhost/information_schema/PLUGINS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"PROCESSLIST","FullyQualifiedName":"localhost/information_schema/PROCESSLIST","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"PROCESSLIST","FullyQualifiedName":"localhost/information_schema/PROCESSLIST","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"PROCESSLIST","FullyQualifiedName":"localhost/information_schema/PROCESSLIST","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"PROCESSLIST","FullyQualifiedName":"localhost/information_schema/PROCESSLIST","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"PROCESSLIST","FullyQualifiedName":"localhost/information_schema/PROCESSLIST","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"PROCESSLIST","FullyQualifiedName":"localhost/information_schema/PROCESSLIST","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"PROCESSLIST","FullyQualifiedName":"localhost/information_schema/PROCESSLIST","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"PROCESSLIST","FullyQualifiedName":"localhost/information_schema/PROCESSLIST","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"PROCESSLIST","FullyQualifiedName":"localhost/information_schema/PROCESSLIST","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"PROCESSLIST","FullyQualifiedName":"localhost/information_schema/PROCESSLIST","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"PROCESSLIST","FullyQualifiedName":"localhost/information_schema/PROCESSLIST","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"PROCESSLIST","FullyQualifiedName":"localhost/information_schema/PROCESSLIST","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"PROFILING","FullyQualifiedName":"localhost/information_schema/PROFILING","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"PROFILING","FullyQualifiedName":"localhost/information_schema/PROFILING","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"PROFILING","FullyQualifiedName":"localhost/information_schema/PROFILING","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"PROFILING","FullyQualifiedName":"localhost/information_schema/PROFILING","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"PROFILING","FullyQualifiedName":"localhost/information_schema/PROFILING","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"PROFILING","FullyQualifiedName":"localhost/information_schema/PROFILING","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"PROFILING","FullyQualifiedName":"localhost/information_schema/PROFILING","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"PROFILING","FullyQualifiedName":"localhost/information_schema/PROFILING","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"PROFILING","FullyQualifiedName":"localhost/information_schema/PROFILING","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"PROFILING","FullyQualifiedName":"localhost/information_schema/PROFILING","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"PROFILING","FullyQualifiedName":"localhost/information_schema/PROFILING","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"PROFILING","FullyQualifiedName":"localhost/information_schema/PROFILING","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"REFERENTIAL_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/REFERENTIAL_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"REFERENTIAL_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/REFERENTIAL_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"REFERENTIAL_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/REFERENTIAL_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"REFERENTIAL_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/REFERENTIAL_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"REFERENTIAL_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/REFERENTIAL_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"REFERENTIAL_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/REFERENTIAL_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"REFERENTIAL_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/REFERENTIAL_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"REFERENTIAL_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/REFERENTIAL_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"REFERENTIAL_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/REFERENTIAL_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"REFERENTIAL_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/REFERENTIAL_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"REFERENTIAL_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/REFERENTIAL_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"REFERENTIAL_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/REFERENTIAL_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"RESOURCE_GROUPS","FullyQualifiedName":"localhost/information_schema/RESOURCE_GROUPS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"RESOURCE_GROUPS","FullyQualifiedName":"localhost/information_schema/RESOURCE_GROUPS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"RESOURCE_GROUPS","FullyQualifiedName":"localhost/information_schema/RESOURCE_GROUPS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"RESOURCE_GROUPS","FullyQualifiedName":"localhost/information_schema/RESOURCE_GROUPS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"RESOURCE_GROUPS","FullyQualifiedName":"localhost/information_schema/RESOURCE_GROUPS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"RESOURCE_GROUPS","FullyQualifiedName":"localhost/information_schema/RESOURCE_GROUPS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"RESOURCE_GROUPS","FullyQualifiedName":"localhost/information_schema/RESOURCE_GROUPS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"RESOURCE_GROUPS","FullyQualifiedName":"localhost/information_schema/RESOURCE_GROUPS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"RESOURCE_GROUPS","FullyQualifiedName":"localhost/information_schema/RESOURCE_GROUPS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"RESOURCE_GROUPS","FullyQualifiedName":"localhost/information_schema/RESOURCE_GROUPS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"RESOURCE_GROUPS","FullyQualifiedName":"localhost/information_schema/RESOURCE_GROUPS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"RESOURCE_GROUPS","FullyQualifiedName":"localhost/information_schema/RESOURCE_GROUPS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"ROLE_COLUMN_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_COLUMN_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"ROLE_COLUMN_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_COLUMN_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"ROLE_COLUMN_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_COLUMN_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"ROLE_COLUMN_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_COLUMN_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"ROLE_COLUMN_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_COLUMN_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"ROLE_COLUMN_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_COLUMN_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"ROLE_COLUMN_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_COLUMN_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"ROLE_COLUMN_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_COLUMN_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"ROLE_COLUMN_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_COLUMN_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"ROLE_COLUMN_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_COLUMN_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"ROLE_COLUMN_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_COLUMN_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"ROLE_COLUMN_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_COLUMN_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"ROLE_ROUTINE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_ROUTINE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"ROLE_ROUTINE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_ROUTINE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"ROLE_ROUTINE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_ROUTINE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"ROLE_ROUTINE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_ROUTINE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"ROLE_ROUTINE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_ROUTINE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"ROLE_ROUTINE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_ROUTINE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"ROLE_ROUTINE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_ROUTINE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"ROLE_ROUTINE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_ROUTINE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"ROLE_ROUTINE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_ROUTINE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"ROLE_ROUTINE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_ROUTINE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"ROLE_ROUTINE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_ROUTINE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"ROLE_ROUTINE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_ROUTINE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"ROLE_TABLE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_TABLE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"ROLE_TABLE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_TABLE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"ROLE_TABLE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_TABLE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"ROLE_TABLE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_TABLE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"ROLE_TABLE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_TABLE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"ROLE_TABLE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_TABLE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"ROLE_TABLE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_TABLE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"ROLE_TABLE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_TABLE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"ROLE_TABLE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_TABLE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"ROLE_TABLE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_TABLE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"ROLE_TABLE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_TABLE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"ROLE_TABLE_GRANTS","FullyQualifiedName":"localhost/information_schema/ROLE_TABLE_GRANTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"ROUTINES","FullyQualifiedName":"localhost/information_schema/ROUTINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"ROUTINES","FullyQualifiedName":"localhost/information_schema/ROUTINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"ROUTINES","FullyQualifiedName":"localhost/information_schema/ROUTINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"ROUTINES","FullyQualifiedName":"localhost/information_schema/ROUTINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"ROUTINES","FullyQualifiedName":"localhost/information_schema/ROUTINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"ROUTINES","FullyQualifiedName":"localhost/information_schema/ROUTINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"ROUTINES","FullyQualifiedName":"localhost/information_schema/ROUTINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"ROUTINES","FullyQualifiedName":"localhost/information_schema/ROUTINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"ROUTINES","FullyQualifiedName":"localhost/information_schema/ROUTINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"ROUTINES","FullyQualifiedName":"localhost/information_schema/ROUTINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"ROUTINES","FullyQualifiedName":"localhost/information_schema/ROUTINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"ROUTINES","FullyQualifiedName":"localhost/information_schema/ROUTINES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"SCHEMATA","FullyQualifiedName":"localhost/information_schema/SCHEMATA","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"SCHEMATA","FullyQualifiedName":"localhost/information_schema/SCHEMATA","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"SCHEMATA","FullyQualifiedName":"localhost/information_schema/SCHEMATA","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"SCHEMATA","FullyQualifiedName":"localhost/information_schema/SCHEMATA","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"SCHEMATA","FullyQualifiedName":"localhost/information_schema/SCHEMATA","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"SCHEMATA","FullyQualifiedName":"localhost/information_schema/SCHEMATA","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"SCHEMATA","FullyQualifiedName":"localhost/information_schema/SCHEMATA","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"SCHEMATA","FullyQualifiedName":"localhost/information_schema/SCHEMATA","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"SCHEMATA","FullyQualifiedName":"localhost/information_schema/SCHEMATA","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"SCHEMATA","FullyQualifiedName":"localhost/information_schema/SCHEMATA","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"SCHEMATA","FullyQualifiedName":"localhost/information_schema/SCHEMATA","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"SCHEMATA","FullyQualifiedName":"localhost/information_schema/SCHEMATA","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"SCHEMATA_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/SCHEMATA_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"SCHEMATA_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/SCHEMATA_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"SCHEMATA_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/SCHEMATA_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"SCHEMATA_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/SCHEMATA_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"SCHEMATA_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/SCHEMATA_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"SCHEMATA_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/SCHEMATA_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"SCHEMATA_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/SCHEMATA_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"SCHEMATA_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/SCHEMATA_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"SCHEMATA_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/SCHEMATA_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"SCHEMATA_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/SCHEMATA_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"SCHEMATA_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/SCHEMATA_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"SCHEMATA_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/SCHEMATA_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"SCHEMA_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/SCHEMA_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"SCHEMA_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/SCHEMA_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"SCHEMA_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/SCHEMA_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"SCHEMA_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/SCHEMA_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"SCHEMA_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/SCHEMA_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"SCHEMA_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/SCHEMA_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"SCHEMA_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/SCHEMA_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"SCHEMA_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/SCHEMA_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"SCHEMA_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/SCHEMA_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"SCHEMA_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/SCHEMA_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"SCHEMA_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/SCHEMA_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"SCHEMA_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/SCHEMA_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"STATISTICS","FullyQualifiedName":"localhost/information_schema/STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"STATISTICS","FullyQualifiedName":"localhost/information_schema/STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"STATISTICS","FullyQualifiedName":"localhost/information_schema/STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"STATISTICS","FullyQualifiedName":"localhost/information_schema/STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"STATISTICS","FullyQualifiedName":"localhost/information_schema/STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"STATISTICS","FullyQualifiedName":"localhost/information_schema/STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"STATISTICS","FullyQualifiedName":"localhost/information_schema/STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"STATISTICS","FullyQualifiedName":"localhost/information_schema/STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"STATISTICS","FullyQualifiedName":"localhost/information_schema/STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"STATISTICS","FullyQualifiedName":"localhost/information_schema/STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"STATISTICS","FullyQualifiedName":"localhost/information_schema/STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"STATISTICS","FullyQualifiedName":"localhost/information_schema/STATISTICS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"ST_GEOMETRY_COLUMNS","FullyQualifiedName":"localhost/information_schema/ST_GEOMETRY_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"ST_GEOMETRY_COLUMNS","FullyQualifiedName":"localhost/information_schema/ST_GEOMETRY_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"ST_GEOMETRY_COLUMNS","FullyQualifiedName":"localhost/information_schema/ST_GEOMETRY_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"ST_GEOMETRY_COLUMNS","FullyQualifiedName":"localhost/information_schema/ST_GEOMETRY_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"ST_GEOMETRY_COLUMNS","FullyQualifiedName":"localhost/information_schema/ST_GEOMETRY_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"ST_GEOMETRY_COLUMNS","FullyQualifiedName":"localhost/information_schema/ST_GEOMETRY_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"ST_GEOMETRY_COLUMNS","FullyQualifiedName":"localhost/information_schema/ST_GEOMETRY_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"ST_GEOMETRY_COLUMNS","FullyQualifiedName":"localhost/information_schema/ST_GEOMETRY_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"ST_GEOMETRY_COLUMNS","FullyQualifiedName":"localhost/information_schema/ST_GEOMETRY_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"ST_GEOMETRY_COLUMNS","FullyQualifiedName":"localhost/information_schema/ST_GEOMETRY_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"ST_GEOMETRY_COLUMNS","FullyQualifiedName":"localhost/information_schema/ST_GEOMETRY_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"ST_GEOMETRY_COLUMNS","FullyQualifiedName":"localhost/information_schema/ST_GEOMETRY_COLUMNS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"ST_SPATIAL_REFERENCE_SYSTEMS","FullyQualifiedName":"localhost/information_schema/ST_SPATIAL_REFERENCE_SYSTEMS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"ST_SPATIAL_REFERENCE_SYSTEMS","FullyQualifiedName":"localhost/information_schema/ST_SPATIAL_REFERENCE_SYSTEMS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"ST_SPATIAL_REFERENCE_SYSTEMS","FullyQualifiedName":"localhost/information_schema/ST_SPATIAL_REFERENCE_SYSTEMS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"ST_SPATIAL_REFERENCE_SYSTEMS","FullyQualifiedName":"localhost/information_schema/ST_SPATIAL_REFERENCE_SYSTEMS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"ST_SPATIAL_REFERENCE_SYSTEMS","FullyQualifiedName":"localhost/information_schema/ST_SPATIAL_REFERENCE_SYSTEMS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"ST_SPATIAL_REFERENCE_SYSTEMS","FullyQualifiedName":"localhost/information_schema/ST_SPATIAL_REFERENCE_SYSTEMS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"ST_SPATIAL_REFERENCE_SYSTEMS","FullyQualifiedName":"localhost/information_schema/ST_SPATIAL_REFERENCE_SYSTEMS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"ST_SPATIAL_REFERENCE_SYSTEMS","FullyQualifiedName":"localhost/information_schema/ST_SPATIAL_REFERENCE_SYSTEMS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"ST_SPATIAL_REFERENCE_SYSTEMS","FullyQualifiedName":"localhost/information_schema/ST_SPATIAL_REFERENCE_SYSTEMS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"ST_SPATIAL_REFERENCE_SYSTEMS","FullyQualifiedName":"localhost/information_schema/ST_SPATIAL_REFERENCE_SYSTEMS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"ST_SPATIAL_REFERENCE_SYSTEMS","FullyQualifiedName":"localhost/information_schema/ST_SPATIAL_REFERENCE_SYSTEMS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"ST_SPATIAL_REFERENCE_SYSTEMS","FullyQualifiedName":"localhost/information_schema/ST_SPATIAL_REFERENCE_SYSTEMS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"ST_UNITS_OF_MEASURE","FullyQualifiedName":"localhost/information_schema/ST_UNITS_OF_MEASURE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"ST_UNITS_OF_MEASURE","FullyQualifiedName":"localhost/information_schema/ST_UNITS_OF_MEASURE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"ST_UNITS_OF_MEASURE","FullyQualifiedName":"localhost/information_schema/ST_UNITS_OF_MEASURE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"ST_UNITS_OF_MEASURE","FullyQualifiedName":"localhost/information_schema/ST_UNITS_OF_MEASURE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"ST_UNITS_OF_MEASURE","FullyQualifiedName":"localhost/information_schema/ST_UNITS_OF_MEASURE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"ST_UNITS_OF_MEASURE","FullyQualifiedName":"localhost/information_schema/ST_UNITS_OF_MEASURE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"ST_UNITS_OF_MEASURE","FullyQualifiedName":"localhost/information_schema/ST_UNITS_OF_MEASURE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"ST_UNITS_OF_MEASURE","FullyQualifiedName":"localhost/information_schema/ST_UNITS_OF_MEASURE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"ST_UNITS_OF_MEASURE","FullyQualifiedName":"localhost/information_schema/ST_UNITS_OF_MEASURE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"ST_UNITS_OF_MEASURE","FullyQualifiedName":"localhost/information_schema/ST_UNITS_OF_MEASURE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"ST_UNITS_OF_MEASURE","FullyQualifiedName":"localhost/information_schema/ST_UNITS_OF_MEASURE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"ST_UNITS_OF_MEASURE","FullyQualifiedName":"localhost/information_schema/ST_UNITS_OF_MEASURE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"TABLES","FullyQualifiedName":"localhost/information_schema/TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"TABLES","FullyQualifiedName":"localhost/information_schema/TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"TABLES","FullyQualifiedName":"localhost/information_schema/TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"TABLES","FullyQualifiedName":"localhost/information_schema/TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"TABLES","FullyQualifiedName":"localhost/information_schema/TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"TABLES","FullyQualifiedName":"localhost/information_schema/TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"TABLES","FullyQualifiedName":"localhost/information_schema/TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"TABLES","FullyQualifiedName":"localhost/information_schema/TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"TABLES","FullyQualifiedName":"localhost/information_schema/TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"TABLES","FullyQualifiedName":"localhost/information_schema/TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"TABLES","FullyQualifiedName":"localhost/information_schema/TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"TABLES","FullyQualifiedName":"localhost/information_schema/TABLES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"TABLESPACES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLESPACES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"TABLESPACES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLESPACES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"TABLESPACES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLESPACES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"TABLESPACES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLESPACES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"TABLESPACES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLESPACES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"TABLESPACES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLESPACES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"TABLESPACES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLESPACES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"TABLESPACES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLESPACES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"TABLESPACES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLESPACES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"TABLESPACES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLESPACES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"TABLESPACES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLESPACES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"TABLESPACES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLESPACES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"TABLES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"TABLES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"TABLES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"TABLES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"TABLES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"TABLES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"TABLES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"TABLES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"TABLES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"TABLES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"TABLES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"TABLES_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLES_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"TABLE_CONSTRAINTS_EXTENSIONS","FullyQualifiedName":"localhost/information_schema/TABLE_CONSTRAINTS_EXTENSIONS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"TABLE_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/TABLE_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"TABLE_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/TABLE_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"TABLE_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/TABLE_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"TABLE_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/TABLE_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"TABLE_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/TABLE_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"TABLE_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/TABLE_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"TABLE_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/TABLE_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"TABLE_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/TABLE_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"TABLE_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/TABLE_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"TABLE_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/TABLE_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"TABLE_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/TABLE_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"TABLE_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/TABLE_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"TRIGGERS","FullyQualifiedName":"localhost/information_schema/TRIGGERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"TRIGGERS","FullyQualifiedName":"localhost/information_schema/TRIGGERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"TRIGGERS","FullyQualifiedName":"localhost/information_schema/TRIGGERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"TRIGGERS","FullyQualifiedName":"localhost/information_schema/TRIGGERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"TRIGGERS","FullyQualifiedName":"localhost/information_schema/TRIGGERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"TRIGGERS","FullyQualifiedName":"localhost/information_schema/TRIGGERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"TRIGGERS","FullyQualifiedName":"localhost/information_schema/TRIGGERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"TRIGGERS","FullyQualifiedName":"localhost/information_schema/TRIGGERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"TRIGGERS","FullyQualifiedName":"localhost/information_schema/TRIGGERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"TRIGGERS","FullyQualifiedName":"localhost/information_schema/TRIGGERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"TRIGGERS","FullyQualifiedName":"localhost/information_schema/TRIGGERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"TRIGGERS","FullyQualifiedName":"localhost/information_schema/TRIGGERS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"USER_ATTRIBUTES","FullyQualifiedName":"localhost/information_schema/USER_ATTRIBUTES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"USER_ATTRIBUTES","FullyQualifiedName":"localhost/information_schema/USER_ATTRIBUTES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"USER_ATTRIBUTES","FullyQualifiedName":"localhost/information_schema/USER_ATTRIBUTES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"USER_ATTRIBUTES","FullyQualifiedName":"localhost/information_schema/USER_ATTRIBUTES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"USER_ATTRIBUTES","FullyQualifiedName":"localhost/information_schema/USER_ATTRIBUTES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"USER_ATTRIBUTES","FullyQualifiedName":"localhost/information_schema/USER_ATTRIBUTES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"USER_ATTRIBUTES","FullyQualifiedName":"localhost/information_schema/USER_ATTRIBUTES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"USER_ATTRIBUTES","FullyQualifiedName":"localhost/information_schema/USER_ATTRIBUTES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"USER_ATTRIBUTES","FullyQualifiedName":"localhost/information_schema/USER_ATTRIBUTES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"USER_ATTRIBUTES","FullyQualifiedName":"localhost/information_schema/USER_ATTRIBUTES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"USER_ATTRIBUTES","FullyQualifiedName":"localhost/information_schema/USER_ATTRIBUTES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"USER_ATTRIBUTES","FullyQualifiedName":"localhost/information_schema/USER_ATTRIBUTES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"USER_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/USER_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"USER_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/USER_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"USER_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/USER_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"USER_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/USER_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"USER_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/USER_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"USER_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/USER_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"USER_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/USER_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"USER_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/USER_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"USER_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/USER_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"USER_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/USER_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"USER_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/USER_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"USER_PRIVILEGES","FullyQualifiedName":"localhost/information_schema/USER_PRIVILEGES","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"VIEWS","FullyQualifiedName":"localhost/information_schema/VIEWS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"VIEWS","FullyQualifiedName":"localhost/information_schema/VIEWS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"VIEWS","FullyQualifiedName":"localhost/information_schema/VIEWS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"VIEWS","FullyQualifiedName":"localhost/information_schema/VIEWS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"VIEWS","FullyQualifiedName":"localhost/information_schema/VIEWS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"VIEWS","FullyQualifiedName":"localhost/information_schema/VIEWS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"VIEWS","FullyQualifiedName":"localhost/information_schema/VIEWS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"VIEWS","FullyQualifiedName":"localhost/information_schema/VIEWS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"VIEWS","FullyQualifiedName":"localhost/information_schema/VIEWS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"VIEWS","FullyQualifiedName":"localhost/information_schema/VIEWS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"VIEWS","FullyQualifiedName":"localhost/information_schema/VIEWS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"VIEWS","FullyQualifiedName":"localhost/information_schema/VIEWS","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"VIEW_ROUTINE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_ROUTINE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"VIEW_ROUTINE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_ROUTINE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"VIEW_ROUTINE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_ROUTINE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"VIEW_ROUTINE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_ROUTINE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"VIEW_ROUTINE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_ROUTINE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"VIEW_ROUTINE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_ROUTINE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"VIEW_ROUTINE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_ROUTINE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"VIEW_ROUTINE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_ROUTINE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"VIEW_ROUTINE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_ROUTINE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"VIEW_ROUTINE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_ROUTINE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"VIEW_ROUTINE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_ROUTINE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"VIEW_ROUTINE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_ROUTINE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"VIEW_TABLE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_TABLE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"VIEW_TABLE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_TABLE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"VIEW_TABLE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_TABLE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"VIEW_TABLE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_TABLE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"VIEW_TABLE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_TABLE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"VIEW_TABLE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_TABLE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"VIEW_TABLE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_TABLE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"VIEW_TABLE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_TABLE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"VIEW_TABLE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_TABLE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"VIEW_TABLE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_TABLE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"VIEW_TABLE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_TABLE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"VIEW_TABLE_USAGE","FullyQualifiedName":"localhost/information_schema/VIEW_TABLE_USAGE","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"information_schema","FullyQualifiedName":"localhost/information_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"accounts","FullyQualifiedName":"localhost/performance_schema/accounts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"accounts","FullyQualifiedName":"localhost/performance_schema/accounts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"accounts","FullyQualifiedName":"localhost/performance_schema/accounts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"accounts","FullyQualifiedName":"localhost/performance_schema/accounts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"accounts","FullyQualifiedName":"localhost/performance_schema/accounts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"accounts","FullyQualifiedName":"localhost/performance_schema/accounts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"accounts","FullyQualifiedName":"localhost/performance_schema/accounts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"accounts","FullyQualifiedName":"localhost/performance_schema/accounts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"accounts","FullyQualifiedName":"localhost/performance_schema/accounts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"accounts","FullyQualifiedName":"localhost/performance_schema/accounts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"accounts","FullyQualifiedName":"localhost/performance_schema/accounts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"accounts","FullyQualifiedName":"localhost/performance_schema/accounts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"binary_log_transaction_compression_stats","FullyQualifiedName":"localhost/performance_schema/binary_log_transaction_compression_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"binary_log_transaction_compression_stats","FullyQualifiedName":"localhost/performance_schema/binary_log_transaction_compression_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"binary_log_transaction_compression_stats","FullyQualifiedName":"localhost/performance_schema/binary_log_transaction_compression_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"binary_log_transaction_compression_stats","FullyQualifiedName":"localhost/performance_schema/binary_log_transaction_compression_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"binary_log_transaction_compression_stats","FullyQualifiedName":"localhost/performance_schema/binary_log_transaction_compression_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"binary_log_transaction_compression_stats","FullyQualifiedName":"localhost/performance_schema/binary_log_transaction_compression_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"binary_log_transaction_compression_stats","FullyQualifiedName":"localhost/performance_schema/binary_log_transaction_compression_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"binary_log_transaction_compression_stats","FullyQualifiedName":"localhost/performance_schema/binary_log_transaction_compression_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"binary_log_transaction_compression_stats","FullyQualifiedName":"localhost/performance_schema/binary_log_transaction_compression_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"binary_log_transaction_compression_stats","FullyQualifiedName":"localhost/performance_schema/binary_log_transaction_compression_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"binary_log_transaction_compression_stats","FullyQualifiedName":"localhost/performance_schema/binary_log_transaction_compression_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"binary_log_transaction_compression_stats","FullyQualifiedName":"localhost/performance_schema/binary_log_transaction_compression_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"columns_priv","FullyQualifiedName":"localhost/mysql/columns_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"columns_priv","FullyQualifiedName":"localhost/mysql/columns_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"columns_priv","FullyQualifiedName":"localhost/mysql/columns_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"columns_priv","FullyQualifiedName":"localhost/mysql/columns_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"columns_priv","FullyQualifiedName":"localhost/mysql/columns_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"columns_priv","FullyQualifiedName":"localhost/mysql/columns_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"columns_priv","FullyQualifiedName":"localhost/mysql/columns_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"columns_priv","FullyQualifiedName":"localhost/mysql/columns_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"columns_priv","FullyQualifiedName":"localhost/mysql/columns_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"columns_priv","FullyQualifiedName":"localhost/mysql/columns_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"columns_priv","FullyQualifiedName":"localhost/mysql/columns_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"columns_priv","FullyQualifiedName":"localhost/mysql/columns_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"component","FullyQualifiedName":"localhost/mysql/component","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"component","FullyQualifiedName":"localhost/mysql/component","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"component","FullyQualifiedName":"localhost/mysql/component","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"component","FullyQualifiedName":"localhost/mysql/component","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"component","FullyQualifiedName":"localhost/mysql/component","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"component","FullyQualifiedName":"localhost/mysql/component","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"component","FullyQualifiedName":"localhost/mysql/component","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"component","FullyQualifiedName":"localhost/mysql/component","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"component","FullyQualifiedName":"localhost/mysql/component","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"component","FullyQualifiedName":"localhost/mysql/component","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"component","FullyQualifiedName":"localhost/mysql/component","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"component","FullyQualifiedName":"localhost/mysql/component","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"cond_instances","FullyQualifiedName":"localhost/performance_schema/cond_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"cond_instances","FullyQualifiedName":"localhost/performance_schema/cond_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"cond_instances","FullyQualifiedName":"localhost/performance_schema/cond_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"cond_instances","FullyQualifiedName":"localhost/performance_schema/cond_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"cond_instances","FullyQualifiedName":"localhost/performance_schema/cond_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"cond_instances","FullyQualifiedName":"localhost/performance_schema/cond_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"cond_instances","FullyQualifiedName":"localhost/performance_schema/cond_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"cond_instances","FullyQualifiedName":"localhost/performance_schema/cond_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"cond_instances","FullyQualifiedName":"localhost/performance_schema/cond_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"cond_instances","FullyQualifiedName":"localhost/performance_schema/cond_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"cond_instances","FullyQualifiedName":"localhost/performance_schema/cond_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"cond_instances","FullyQualifiedName":"localhost/performance_schema/cond_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"create_synonym_db","FullyQualifiedName":"localhost/sys/create_synonym_db","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"create_synonym_db","FullyQualifiedName":"localhost/sys/create_synonym_db","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"data_lock_waits","FullyQualifiedName":"localhost/performance_schema/data_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"data_lock_waits","FullyQualifiedName":"localhost/performance_schema/data_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"data_lock_waits","FullyQualifiedName":"localhost/performance_schema/data_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"data_lock_waits","FullyQualifiedName":"localhost/performance_schema/data_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"data_lock_waits","FullyQualifiedName":"localhost/performance_schema/data_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"data_lock_waits","FullyQualifiedName":"localhost/performance_schema/data_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"data_lock_waits","FullyQualifiedName":"localhost/performance_schema/data_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"data_lock_waits","FullyQualifiedName":"localhost/performance_schema/data_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"data_lock_waits","FullyQualifiedName":"localhost/performance_schema/data_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"data_lock_waits","FullyQualifiedName":"localhost/performance_schema/data_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"data_lock_waits","FullyQualifiedName":"localhost/performance_schema/data_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"data_lock_waits","FullyQualifiedName":"localhost/performance_schema/data_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"data_locks","FullyQualifiedName":"localhost/performance_schema/data_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"data_locks","FullyQualifiedName":"localhost/performance_schema/data_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"data_locks","FullyQualifiedName":"localhost/performance_schema/data_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"data_locks","FullyQualifiedName":"localhost/performance_schema/data_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"data_locks","FullyQualifiedName":"localhost/performance_schema/data_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"data_locks","FullyQualifiedName":"localhost/performance_schema/data_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"data_locks","FullyQualifiedName":"localhost/performance_schema/data_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"data_locks","FullyQualifiedName":"localhost/performance_schema/data_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"data_locks","FullyQualifiedName":"localhost/performance_schema/data_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"data_locks","FullyQualifiedName":"localhost/performance_schema/data_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"data_locks","FullyQualifiedName":"localhost/performance_schema/data_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"data_locks","FullyQualifiedName":"localhost/performance_schema/data_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"db","FullyQualifiedName":"localhost/mysql/db","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"db","FullyQualifiedName":"localhost/mysql/db","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"db","FullyQualifiedName":"localhost/mysql/db","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"db","FullyQualifiedName":"localhost/mysql/db","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"db","FullyQualifiedName":"localhost/mysql/db","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"db","FullyQualifiedName":"localhost/mysql/db","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"db","FullyQualifiedName":"localhost/mysql/db","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"db","FullyQualifiedName":"localhost/mysql/db","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"db","FullyQualifiedName":"localhost/mysql/db","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"db","FullyQualifiedName":"localhost/mysql/db","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"db","FullyQualifiedName":"localhost/mysql/db","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"db","FullyQualifiedName":"localhost/mysql/db","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"default_roles","FullyQualifiedName":"localhost/mysql/default_roles","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"default_roles","FullyQualifiedName":"localhost/mysql/default_roles","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"default_roles","FullyQualifiedName":"localhost/mysql/default_roles","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"default_roles","FullyQualifiedName":"localhost/mysql/default_roles","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"default_roles","FullyQualifiedName":"localhost/mysql/default_roles","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"default_roles","FullyQualifiedName":"localhost/mysql/default_roles","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"default_roles","FullyQualifiedName":"localhost/mysql/default_roles","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"default_roles","FullyQualifiedName":"localhost/mysql/default_roles","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"default_roles","FullyQualifiedName":"localhost/mysql/default_roles","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"default_roles","FullyQualifiedName":"localhost/mysql/default_roles","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"default_roles","FullyQualifiedName":"localhost/mysql/default_roles","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"default_roles","FullyQualifiedName":"localhost/mysql/default_roles","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"diagnostics","FullyQualifiedName":"localhost/sys/diagnostics","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"diagnostics","FullyQualifiedName":"localhost/sys/diagnostics","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"engine_cost","FullyQualifiedName":"localhost/mysql/engine_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"engine_cost","FullyQualifiedName":"localhost/mysql/engine_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"engine_cost","FullyQualifiedName":"localhost/mysql/engine_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"engine_cost","FullyQualifiedName":"localhost/mysql/engine_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"engine_cost","FullyQualifiedName":"localhost/mysql/engine_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"engine_cost","FullyQualifiedName":"localhost/mysql/engine_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"engine_cost","FullyQualifiedName":"localhost/mysql/engine_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"engine_cost","FullyQualifiedName":"localhost/mysql/engine_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"engine_cost","FullyQualifiedName":"localhost/mysql/engine_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"engine_cost","FullyQualifiedName":"localhost/mysql/engine_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"engine_cost","FullyQualifiedName":"localhost/mysql/engine_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"engine_cost","FullyQualifiedName":"localhost/mysql/engine_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"error_log","FullyQualifiedName":"localhost/performance_schema/error_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"error_log","FullyQualifiedName":"localhost/performance_schema/error_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"error_log","FullyQualifiedName":"localhost/performance_schema/error_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"error_log","FullyQualifiedName":"localhost/performance_schema/error_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"error_log","FullyQualifiedName":"localhost/performance_schema/error_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"error_log","FullyQualifiedName":"localhost/performance_schema/error_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"error_log","FullyQualifiedName":"localhost/performance_schema/error_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"error_log","FullyQualifiedName":"localhost/performance_schema/error_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"error_log","FullyQualifiedName":"localhost/performance_schema/error_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"error_log","FullyQualifiedName":"localhost/performance_schema/error_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"error_log","FullyQualifiedName":"localhost/performance_schema/error_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"error_log","FullyQualifiedName":"localhost/performance_schema/error_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_account_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_account_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_account_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_account_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_account_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_account_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_account_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_account_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_account_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_account_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_account_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_account_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_account_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_account_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_account_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_account_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_account_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_account_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_account_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_account_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_account_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_account_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_account_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_account_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_host_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_host_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_host_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_host_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_host_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_host_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_host_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_host_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_host_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_host_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_host_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_host_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_host_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_host_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_host_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_host_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_host_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_host_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_host_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_host_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_host_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_host_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_host_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_host_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_thread_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_thread_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_thread_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_thread_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_thread_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_thread_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_thread_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_thread_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_thread_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_thread_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_thread_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_thread_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_thread_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_thread_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_thread_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_thread_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_thread_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_thread_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_thread_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_thread_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_thread_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_thread_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_thread_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_thread_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_user_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_user_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_user_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_user_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_user_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_user_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_user_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_user_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_user_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_user_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_user_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_user_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_user_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_user_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_user_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_user_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_user_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_user_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_user_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_user_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_user_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_user_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_errors_summary_by_user_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_by_user_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_errors_summary_global_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_global_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_errors_summary_global_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_global_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_errors_summary_global_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_global_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_errors_summary_global_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_global_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_errors_summary_global_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_global_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_errors_summary_global_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_global_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_errors_summary_global_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_global_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_errors_summary_global_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_global_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_errors_summary_global_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_global_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_errors_summary_global_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_global_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_errors_summary_global_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_global_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_errors_summary_global_by_error","FullyQualifiedName":"localhost/performance_schema/events_errors_summary_global_by_error","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_stages_current","FullyQualifiedName":"localhost/performance_schema/events_stages_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_stages_current","FullyQualifiedName":"localhost/performance_schema/events_stages_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_stages_current","FullyQualifiedName":"localhost/performance_schema/events_stages_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_stages_current","FullyQualifiedName":"localhost/performance_schema/events_stages_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_stages_current","FullyQualifiedName":"localhost/performance_schema/events_stages_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_stages_current","FullyQualifiedName":"localhost/performance_schema/events_stages_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_stages_current","FullyQualifiedName":"localhost/performance_schema/events_stages_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_stages_current","FullyQualifiedName":"localhost/performance_schema/events_stages_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_stages_current","FullyQualifiedName":"localhost/performance_schema/events_stages_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_stages_current","FullyQualifiedName":"localhost/performance_schema/events_stages_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_stages_current","FullyQualifiedName":"localhost/performance_schema/events_stages_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_stages_current","FullyQualifiedName":"localhost/performance_schema/events_stages_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_stages_history","FullyQualifiedName":"localhost/performance_schema/events_stages_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_stages_history","FullyQualifiedName":"localhost/performance_schema/events_stages_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_stages_history","FullyQualifiedName":"localhost/performance_schema/events_stages_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_stages_history","FullyQualifiedName":"localhost/performance_schema/events_stages_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_stages_history","FullyQualifiedName":"localhost/performance_schema/events_stages_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_stages_history","FullyQualifiedName":"localhost/performance_schema/events_stages_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_stages_history","FullyQualifiedName":"localhost/performance_schema/events_stages_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_stages_history","FullyQualifiedName":"localhost/performance_schema/events_stages_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_stages_history","FullyQualifiedName":"localhost/performance_schema/events_stages_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_stages_history","FullyQualifiedName":"localhost/performance_schema/events_stages_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_stages_history","FullyQualifiedName":"localhost/performance_schema/events_stages_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_stages_history","FullyQualifiedName":"localhost/performance_schema/events_stages_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_stages_history_long","FullyQualifiedName":"localhost/performance_schema/events_stages_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_stages_history_long","FullyQualifiedName":"localhost/performance_schema/events_stages_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_stages_history_long","FullyQualifiedName":"localhost/performance_schema/events_stages_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_stages_history_long","FullyQualifiedName":"localhost/performance_schema/events_stages_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_stages_history_long","FullyQualifiedName":"localhost/performance_schema/events_stages_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_stages_history_long","FullyQualifiedName":"localhost/performance_schema/events_stages_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_stages_history_long","FullyQualifiedName":"localhost/performance_schema/events_stages_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_stages_history_long","FullyQualifiedName":"localhost/performance_schema/events_stages_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_stages_history_long","FullyQualifiedName":"localhost/performance_schema/events_stages_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_stages_history_long","FullyQualifiedName":"localhost/performance_schema/events_stages_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_stages_history_long","FullyQualifiedName":"localhost/performance_schema/events_stages_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_stages_history_long","FullyQualifiedName":"localhost/performance_schema/events_stages_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_stages_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_stages_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_stages_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_stages_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_stages_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_stages_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_stages_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_stages_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_stages_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_stages_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_stages_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_stages_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_stages_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_stages_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_statements_current","FullyQualifiedName":"localhost/performance_schema/events_statements_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_statements_current","FullyQualifiedName":"localhost/performance_schema/events_statements_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_statements_current","FullyQualifiedName":"localhost/performance_schema/events_statements_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_statements_current","FullyQualifiedName":"localhost/performance_schema/events_statements_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_statements_current","FullyQualifiedName":"localhost/performance_schema/events_statements_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_statements_current","FullyQualifiedName":"localhost/performance_schema/events_statements_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_statements_current","FullyQualifiedName":"localhost/performance_schema/events_statements_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_statements_current","FullyQualifiedName":"localhost/performance_schema/events_statements_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_statements_current","FullyQualifiedName":"localhost/performance_schema/events_statements_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_statements_current","FullyQualifiedName":"localhost/performance_schema/events_statements_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_statements_current","FullyQualifiedName":"localhost/performance_schema/events_statements_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_statements_current","FullyQualifiedName":"localhost/performance_schema/events_statements_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_statements_histogram_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_statements_histogram_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_statements_histogram_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_statements_histogram_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_statements_histogram_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_statements_histogram_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_statements_histogram_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_statements_histogram_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_statements_histogram_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_statements_histogram_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_statements_histogram_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_statements_histogram_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_statements_histogram_global","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_global","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_statements_histogram_global","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_global","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_statements_histogram_global","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_global","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_statements_histogram_global","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_global","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_statements_histogram_global","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_global","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_statements_histogram_global","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_global","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_statements_histogram_global","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_global","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_statements_histogram_global","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_global","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_statements_histogram_global","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_global","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_statements_histogram_global","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_global","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_statements_histogram_global","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_global","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_statements_histogram_global","FullyQualifiedName":"localhost/performance_schema/events_statements_histogram_global","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_statements_history","FullyQualifiedName":"localhost/performance_schema/events_statements_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_statements_history","FullyQualifiedName":"localhost/performance_schema/events_statements_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_statements_history","FullyQualifiedName":"localhost/performance_schema/events_statements_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_statements_history","FullyQualifiedName":"localhost/performance_schema/events_statements_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_statements_history","FullyQualifiedName":"localhost/performance_schema/events_statements_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_statements_history","FullyQualifiedName":"localhost/performance_schema/events_statements_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_statements_history","FullyQualifiedName":"localhost/performance_schema/events_statements_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_statements_history","FullyQualifiedName":"localhost/performance_schema/events_statements_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_statements_history","FullyQualifiedName":"localhost/performance_schema/events_statements_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_statements_history","FullyQualifiedName":"localhost/performance_schema/events_statements_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_statements_history","FullyQualifiedName":"localhost/performance_schema/events_statements_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_statements_history","FullyQualifiedName":"localhost/performance_schema/events_statements_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_statements_history_long","FullyQualifiedName":"localhost/performance_schema/events_statements_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_statements_history_long","FullyQualifiedName":"localhost/performance_schema/events_statements_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_statements_history_long","FullyQualifiedName":"localhost/performance_schema/events_statements_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_statements_history_long","FullyQualifiedName":"localhost/performance_schema/events_statements_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_statements_history_long","FullyQualifiedName":"localhost/performance_schema/events_statements_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_statements_history_long","FullyQualifiedName":"localhost/performance_schema/events_statements_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_statements_history_long","FullyQualifiedName":"localhost/performance_schema/events_statements_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_statements_history_long","FullyQualifiedName":"localhost/performance_schema/events_statements_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_statements_history_long","FullyQualifiedName":"localhost/performance_schema/events_statements_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_statements_history_long","FullyQualifiedName":"localhost/performance_schema/events_statements_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_statements_history_long","FullyQualifiedName":"localhost/performance_schema/events_statements_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_statements_history_long","FullyQualifiedName":"localhost/performance_schema/events_statements_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_digest","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_digest","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_program","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_program","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_program","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_program","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_program","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_program","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_program","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_program","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_program","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_program","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_program","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_program","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_program","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_program","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_program","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_program","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_program","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_program","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_program","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_program","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_program","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_program","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_program","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_program","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_statements_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_statements_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_statements_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_statements_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_statements_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_statements_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_statements_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_statements_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_statements_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_statements_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_statements_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_statements_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_statements_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_statements_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_transactions_current","FullyQualifiedName":"localhost/performance_schema/events_transactions_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_transactions_current","FullyQualifiedName":"localhost/performance_schema/events_transactions_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_transactions_current","FullyQualifiedName":"localhost/performance_schema/events_transactions_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_current","FullyQualifiedName":"localhost/performance_schema/events_transactions_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_transactions_current","FullyQualifiedName":"localhost/performance_schema/events_transactions_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_transactions_current","FullyQualifiedName":"localhost/performance_schema/events_transactions_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_transactions_current","FullyQualifiedName":"localhost/performance_schema/events_transactions_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_transactions_current","FullyQualifiedName":"localhost/performance_schema/events_transactions_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_transactions_current","FullyQualifiedName":"localhost/performance_schema/events_transactions_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_transactions_current","FullyQualifiedName":"localhost/performance_schema/events_transactions_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_current","FullyQualifiedName":"localhost/performance_schema/events_transactions_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_transactions_current","FullyQualifiedName":"localhost/performance_schema/events_transactions_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_transactions_history","FullyQualifiedName":"localhost/performance_schema/events_transactions_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_transactions_history","FullyQualifiedName":"localhost/performance_schema/events_transactions_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_transactions_history","FullyQualifiedName":"localhost/performance_schema/events_transactions_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_history","FullyQualifiedName":"localhost/performance_schema/events_transactions_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_transactions_history","FullyQualifiedName":"localhost/performance_schema/events_transactions_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_transactions_history","FullyQualifiedName":"localhost/performance_schema/events_transactions_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_transactions_history","FullyQualifiedName":"localhost/performance_schema/events_transactions_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_transactions_history","FullyQualifiedName":"localhost/performance_schema/events_transactions_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_transactions_history","FullyQualifiedName":"localhost/performance_schema/events_transactions_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_transactions_history","FullyQualifiedName":"localhost/performance_schema/events_transactions_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_history","FullyQualifiedName":"localhost/performance_schema/events_transactions_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_transactions_history","FullyQualifiedName":"localhost/performance_schema/events_transactions_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_transactions_history_long","FullyQualifiedName":"localhost/performance_schema/events_transactions_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_transactions_history_long","FullyQualifiedName":"localhost/performance_schema/events_transactions_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_transactions_history_long","FullyQualifiedName":"localhost/performance_schema/events_transactions_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_history_long","FullyQualifiedName":"localhost/performance_schema/events_transactions_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_transactions_history_long","FullyQualifiedName":"localhost/performance_schema/events_transactions_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_transactions_history_long","FullyQualifiedName":"localhost/performance_schema/events_transactions_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_transactions_history_long","FullyQualifiedName":"localhost/performance_schema/events_transactions_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_transactions_history_long","FullyQualifiedName":"localhost/performance_schema/events_transactions_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_transactions_history_long","FullyQualifiedName":"localhost/performance_schema/events_transactions_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_transactions_history_long","FullyQualifiedName":"localhost/performance_schema/events_transactions_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_history_long","FullyQualifiedName":"localhost/performance_schema/events_transactions_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_transactions_history_long","FullyQualifiedName":"localhost/performance_schema/events_transactions_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_transactions_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_transactions_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_transactions_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_transactions_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_transactions_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_transactions_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_transactions_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_transactions_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_transactions_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_transactions_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_transactions_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_transactions_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_transactions_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_waits_current","FullyQualifiedName":"localhost/performance_schema/events_waits_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_waits_current","FullyQualifiedName":"localhost/performance_schema/events_waits_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_waits_current","FullyQualifiedName":"localhost/performance_schema/events_waits_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_waits_current","FullyQualifiedName":"localhost/performance_schema/events_waits_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_waits_current","FullyQualifiedName":"localhost/performance_schema/events_waits_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_waits_current","FullyQualifiedName":"localhost/performance_schema/events_waits_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_waits_current","FullyQualifiedName":"localhost/performance_schema/events_waits_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_waits_current","FullyQualifiedName":"localhost/performance_schema/events_waits_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_waits_current","FullyQualifiedName":"localhost/performance_schema/events_waits_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_waits_current","FullyQualifiedName":"localhost/performance_schema/events_waits_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_waits_current","FullyQualifiedName":"localhost/performance_schema/events_waits_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_waits_current","FullyQualifiedName":"localhost/performance_schema/events_waits_current","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_waits_history","FullyQualifiedName":"localhost/performance_schema/events_waits_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_waits_history","FullyQualifiedName":"localhost/performance_schema/events_waits_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_waits_history","FullyQualifiedName":"localhost/performance_schema/events_waits_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_waits_history","FullyQualifiedName":"localhost/performance_schema/events_waits_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_waits_history","FullyQualifiedName":"localhost/performance_schema/events_waits_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_waits_history","FullyQualifiedName":"localhost/performance_schema/events_waits_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_waits_history","FullyQualifiedName":"localhost/performance_schema/events_waits_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_waits_history","FullyQualifiedName":"localhost/performance_schema/events_waits_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_waits_history","FullyQualifiedName":"localhost/performance_schema/events_waits_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_waits_history","FullyQualifiedName":"localhost/performance_schema/events_waits_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_waits_history","FullyQualifiedName":"localhost/performance_schema/events_waits_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_waits_history","FullyQualifiedName":"localhost/performance_schema/events_waits_history","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_waits_history_long","FullyQualifiedName":"localhost/performance_schema/events_waits_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_waits_history_long","FullyQualifiedName":"localhost/performance_schema/events_waits_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_waits_history_long","FullyQualifiedName":"localhost/performance_schema/events_waits_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_waits_history_long","FullyQualifiedName":"localhost/performance_schema/events_waits_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_waits_history_long","FullyQualifiedName":"localhost/performance_schema/events_waits_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_waits_history_long","FullyQualifiedName":"localhost/performance_schema/events_waits_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_waits_history_long","FullyQualifiedName":"localhost/performance_schema/events_waits_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_waits_history_long","FullyQualifiedName":"localhost/performance_schema/events_waits_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_waits_history_long","FullyQualifiedName":"localhost/performance_schema/events_waits_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_waits_history_long","FullyQualifiedName":"localhost/performance_schema/events_waits_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_waits_history_long","FullyQualifiedName":"localhost/performance_schema/events_waits_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_waits_history_long","FullyQualifiedName":"localhost/performance_schema/events_waits_history_long","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_waits_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"events_waits_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"events_waits_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"events_waits_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"events_waits_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"events_waits_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"events_waits_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"events_waits_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"events_waits_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"events_waits_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"events_waits_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"events_waits_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"events_waits_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/events_waits_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"execute_prepared_stmt","FullyQualifiedName":"localhost/sys/execute_prepared_stmt","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"execute_prepared_stmt","FullyQualifiedName":"localhost/sys/execute_prepared_stmt","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"extract_schema_from_file_name","FullyQualifiedName":"localhost/sys/extract_schema_from_file_name","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"extract_schema_from_file_name","FullyQualifiedName":"localhost/sys/extract_schema_from_file_name","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"extract_table_from_file_name","FullyQualifiedName":"localhost/sys/extract_table_from_file_name","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"extract_table_from_file_name","FullyQualifiedName":"localhost/sys/extract_table_from_file_name","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"file_instances","FullyQualifiedName":"localhost/performance_schema/file_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"file_instances","FullyQualifiedName":"localhost/performance_schema/file_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"file_instances","FullyQualifiedName":"localhost/performance_schema/file_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"file_instances","FullyQualifiedName":"localhost/performance_schema/file_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"file_instances","FullyQualifiedName":"localhost/performance_schema/file_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"file_instances","FullyQualifiedName":"localhost/performance_schema/file_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"file_instances","FullyQualifiedName":"localhost/performance_schema/file_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"file_instances","FullyQualifiedName":"localhost/performance_schema/file_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"file_instances","FullyQualifiedName":"localhost/performance_schema/file_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"file_instances","FullyQualifiedName":"localhost/performance_schema/file_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"file_instances","FullyQualifiedName":"localhost/performance_schema/file_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"file_instances","FullyQualifiedName":"localhost/performance_schema/file_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"file_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/file_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"file_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/file_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"file_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/file_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"file_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/file_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"file_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/file_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"file_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/file_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"file_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/file_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"file_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/file_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"file_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/file_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"file_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/file_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"file_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/file_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"file_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/file_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"file_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/file_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"file_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/file_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"file_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/file_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"file_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/file_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"file_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/file_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"file_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/file_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"file_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/file_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"file_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/file_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"file_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/file_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"file_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/file_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"file_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/file_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"file_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/file_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"format_bytes","FullyQualifiedName":"localhost/sys/format_bytes","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"format_bytes","FullyQualifiedName":"localhost/sys/format_bytes","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"format_path","FullyQualifiedName":"localhost/sys/format_path","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"format_path","FullyQualifiedName":"localhost/sys/format_path","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"format_statement","FullyQualifiedName":"localhost/sys/format_statement","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"format_statement","FullyQualifiedName":"localhost/sys/format_statement","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"format_time","FullyQualifiedName":"localhost/sys/format_time","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"format_time","FullyQualifiedName":"localhost/sys/format_time","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"func","FullyQualifiedName":"localhost/mysql/func","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"func","FullyQualifiedName":"localhost/mysql/func","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"func","FullyQualifiedName":"localhost/mysql/func","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"func","FullyQualifiedName":"localhost/mysql/func","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"func","FullyQualifiedName":"localhost/mysql/func","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"func","FullyQualifiedName":"localhost/mysql/func","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"func","FullyQualifiedName":"localhost/mysql/func","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"func","FullyQualifiedName":"localhost/mysql/func","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"func","FullyQualifiedName":"localhost/mysql/func","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"func","FullyQualifiedName":"localhost/mysql/func","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"func","FullyQualifiedName":"localhost/mysql/func","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"func","FullyQualifiedName":"localhost/mysql/func","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"general_log","FullyQualifiedName":"localhost/mysql/general_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"general_log","FullyQualifiedName":"localhost/mysql/general_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"general_log","FullyQualifiedName":"localhost/mysql/general_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"general_log","FullyQualifiedName":"localhost/mysql/general_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"general_log","FullyQualifiedName":"localhost/mysql/general_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"general_log","FullyQualifiedName":"localhost/mysql/general_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"general_log","FullyQualifiedName":"localhost/mysql/general_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"general_log","FullyQualifiedName":"localhost/mysql/general_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"general_log","FullyQualifiedName":"localhost/mysql/general_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"general_log","FullyQualifiedName":"localhost/mysql/general_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"general_log","FullyQualifiedName":"localhost/mysql/general_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"general_log","FullyQualifiedName":"localhost/mysql/general_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"global_grants","FullyQualifiedName":"localhost/mysql/global_grants","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"global_grants","FullyQualifiedName":"localhost/mysql/global_grants","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"global_grants","FullyQualifiedName":"localhost/mysql/global_grants","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"global_grants","FullyQualifiedName":"localhost/mysql/global_grants","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"global_grants","FullyQualifiedName":"localhost/mysql/global_grants","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"global_grants","FullyQualifiedName":"localhost/mysql/global_grants","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"global_grants","FullyQualifiedName":"localhost/mysql/global_grants","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"global_grants","FullyQualifiedName":"localhost/mysql/global_grants","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"global_grants","FullyQualifiedName":"localhost/mysql/global_grants","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"global_grants","FullyQualifiedName":"localhost/mysql/global_grants","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"global_grants","FullyQualifiedName":"localhost/mysql/global_grants","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"global_grants","FullyQualifiedName":"localhost/mysql/global_grants","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"global_status","FullyQualifiedName":"localhost/performance_schema/global_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"global_status","FullyQualifiedName":"localhost/performance_schema/global_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"global_status","FullyQualifiedName":"localhost/performance_schema/global_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"global_status","FullyQualifiedName":"localhost/performance_schema/global_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"global_status","FullyQualifiedName":"localhost/performance_schema/global_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"global_status","FullyQualifiedName":"localhost/performance_schema/global_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"global_status","FullyQualifiedName":"localhost/performance_schema/global_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"global_status","FullyQualifiedName":"localhost/performance_schema/global_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"global_status","FullyQualifiedName":"localhost/performance_schema/global_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"global_status","FullyQualifiedName":"localhost/performance_schema/global_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"global_status","FullyQualifiedName":"localhost/performance_schema/global_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"global_status","FullyQualifiedName":"localhost/performance_schema/global_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"global_variable_attributes","FullyQualifiedName":"localhost/performance_schema/global_variable_attributes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"global_variable_attributes","FullyQualifiedName":"localhost/performance_schema/global_variable_attributes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"global_variable_attributes","FullyQualifiedName":"localhost/performance_schema/global_variable_attributes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"global_variable_attributes","FullyQualifiedName":"localhost/performance_schema/global_variable_attributes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"global_variable_attributes","FullyQualifiedName":"localhost/performance_schema/global_variable_attributes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"global_variable_attributes","FullyQualifiedName":"localhost/performance_schema/global_variable_attributes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"global_variable_attributes","FullyQualifiedName":"localhost/performance_schema/global_variable_attributes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"global_variable_attributes","FullyQualifiedName":"localhost/performance_schema/global_variable_attributes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"global_variable_attributes","FullyQualifiedName":"localhost/performance_schema/global_variable_attributes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"global_variable_attributes","FullyQualifiedName":"localhost/performance_schema/global_variable_attributes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"global_variable_attributes","FullyQualifiedName":"localhost/performance_schema/global_variable_attributes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"global_variable_attributes","FullyQualifiedName":"localhost/performance_schema/global_variable_attributes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"global_variables","FullyQualifiedName":"localhost/performance_schema/global_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"global_variables","FullyQualifiedName":"localhost/performance_schema/global_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"global_variables","FullyQualifiedName":"localhost/performance_schema/global_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"global_variables","FullyQualifiedName":"localhost/performance_schema/global_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"global_variables","FullyQualifiedName":"localhost/performance_schema/global_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"global_variables","FullyQualifiedName":"localhost/performance_schema/global_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"global_variables","FullyQualifiedName":"localhost/performance_schema/global_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"global_variables","FullyQualifiedName":"localhost/performance_schema/global_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"global_variables","FullyQualifiedName":"localhost/performance_schema/global_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"global_variables","FullyQualifiedName":"localhost/performance_schema/global_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"global_variables","FullyQualifiedName":"localhost/performance_schema/global_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"global_variables","FullyQualifiedName":"localhost/performance_schema/global_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"gtid_executed","FullyQualifiedName":"localhost/mysql/gtid_executed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"gtid_executed","FullyQualifiedName":"localhost/mysql/gtid_executed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"gtid_executed","FullyQualifiedName":"localhost/mysql/gtid_executed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"gtid_executed","FullyQualifiedName":"localhost/mysql/gtid_executed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"gtid_executed","FullyQualifiedName":"localhost/mysql/gtid_executed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"gtid_executed","FullyQualifiedName":"localhost/mysql/gtid_executed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"gtid_executed","FullyQualifiedName":"localhost/mysql/gtid_executed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"gtid_executed","FullyQualifiedName":"localhost/mysql/gtid_executed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"gtid_executed","FullyQualifiedName":"localhost/mysql/gtid_executed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"gtid_executed","FullyQualifiedName":"localhost/mysql/gtid_executed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"gtid_executed","FullyQualifiedName":"localhost/mysql/gtid_executed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"gtid_executed","FullyQualifiedName":"localhost/mysql/gtid_executed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"help_category","FullyQualifiedName":"localhost/mysql/help_category","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"help_category","FullyQualifiedName":"localhost/mysql/help_category","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"help_category","FullyQualifiedName":"localhost/mysql/help_category","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"help_category","FullyQualifiedName":"localhost/mysql/help_category","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"help_category","FullyQualifiedName":"localhost/mysql/help_category","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"help_category","FullyQualifiedName":"localhost/mysql/help_category","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"help_category","FullyQualifiedName":"localhost/mysql/help_category","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"help_category","FullyQualifiedName":"localhost/mysql/help_category","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"help_category","FullyQualifiedName":"localhost/mysql/help_category","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"help_category","FullyQualifiedName":"localhost/mysql/help_category","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"help_category","FullyQualifiedName":"localhost/mysql/help_category","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"help_category","FullyQualifiedName":"localhost/mysql/help_category","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"help_keyword","FullyQualifiedName":"localhost/mysql/help_keyword","Type":"table","Metadata":{"bytes":131072,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"help_keyword","FullyQualifiedName":"localhost/mysql/help_keyword","Type":"table","Metadata":{"bytes":131072,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"help_keyword","FullyQualifiedName":"localhost/mysql/help_keyword","Type":"table","Metadata":{"bytes":131072,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"help_keyword","FullyQualifiedName":"localhost/mysql/help_keyword","Type":"table","Metadata":{"bytes":131072,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"help_keyword","FullyQualifiedName":"localhost/mysql/help_keyword","Type":"table","Metadata":{"bytes":131072,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"help_keyword","FullyQualifiedName":"localhost/mysql/help_keyword","Type":"table","Metadata":{"bytes":131072,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"help_keyword","FullyQualifiedName":"localhost/mysql/help_keyword","Type":"table","Metadata":{"bytes":131072,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"help_keyword","FullyQualifiedName":"localhost/mysql/help_keyword","Type":"table","Metadata":{"bytes":131072,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"help_keyword","FullyQualifiedName":"localhost/mysql/help_keyword","Type":"table","Metadata":{"bytes":131072,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"help_keyword","FullyQualifiedName":"localhost/mysql/help_keyword","Type":"table","Metadata":{"bytes":131072,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"help_keyword","FullyQualifiedName":"localhost/mysql/help_keyword","Type":"table","Metadata":{"bytes":131072,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"help_keyword","FullyQualifiedName":"localhost/mysql/help_keyword","Type":"table","Metadata":{"bytes":131072,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"help_relation","FullyQualifiedName":"localhost/mysql/help_relation","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"help_relation","FullyQualifiedName":"localhost/mysql/help_relation","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"help_relation","FullyQualifiedName":"localhost/mysql/help_relation","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"help_relation","FullyQualifiedName":"localhost/mysql/help_relation","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"help_relation","FullyQualifiedName":"localhost/mysql/help_relation","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"help_relation","FullyQualifiedName":"localhost/mysql/help_relation","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"help_relation","FullyQualifiedName":"localhost/mysql/help_relation","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"help_relation","FullyQualifiedName":"localhost/mysql/help_relation","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"help_relation","FullyQualifiedName":"localhost/mysql/help_relation","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"help_relation","FullyQualifiedName":"localhost/mysql/help_relation","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"help_relation","FullyQualifiedName":"localhost/mysql/help_relation","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"help_relation","FullyQualifiedName":"localhost/mysql/help_relation","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"help_topic","FullyQualifiedName":"localhost/mysql/help_topic","Type":"table","Metadata":{"bytes":1589248,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"help_topic","FullyQualifiedName":"localhost/mysql/help_topic","Type":"table","Metadata":{"bytes":1589248,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"help_topic","FullyQualifiedName":"localhost/mysql/help_topic","Type":"table","Metadata":{"bytes":1589248,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"help_topic","FullyQualifiedName":"localhost/mysql/help_topic","Type":"table","Metadata":{"bytes":1589248,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"help_topic","FullyQualifiedName":"localhost/mysql/help_topic","Type":"table","Metadata":{"bytes":1589248,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"help_topic","FullyQualifiedName":"localhost/mysql/help_topic","Type":"table","Metadata":{"bytes":1589248,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"help_topic","FullyQualifiedName":"localhost/mysql/help_topic","Type":"table","Metadata":{"bytes":1589248,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"help_topic","FullyQualifiedName":"localhost/mysql/help_topic","Type":"table","Metadata":{"bytes":1589248,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"help_topic","FullyQualifiedName":"localhost/mysql/help_topic","Type":"table","Metadata":{"bytes":1589248,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"help_topic","FullyQualifiedName":"localhost/mysql/help_topic","Type":"table","Metadata":{"bytes":1589248,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"help_topic","FullyQualifiedName":"localhost/mysql/help_topic","Type":"table","Metadata":{"bytes":1589248,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"help_topic","FullyQualifiedName":"localhost/mysql/help_topic","Type":"table","Metadata":{"bytes":1589248,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"host_cache","FullyQualifiedName":"localhost/performance_schema/host_cache","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"host_cache","FullyQualifiedName":"localhost/performance_schema/host_cache","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"host_cache","FullyQualifiedName":"localhost/performance_schema/host_cache","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"host_cache","FullyQualifiedName":"localhost/performance_schema/host_cache","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"host_cache","FullyQualifiedName":"localhost/performance_schema/host_cache","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"host_cache","FullyQualifiedName":"localhost/performance_schema/host_cache","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"host_cache","FullyQualifiedName":"localhost/performance_schema/host_cache","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"host_cache","FullyQualifiedName":"localhost/performance_schema/host_cache","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"host_cache","FullyQualifiedName":"localhost/performance_schema/host_cache","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"host_cache","FullyQualifiedName":"localhost/performance_schema/host_cache","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"host_cache","FullyQualifiedName":"localhost/performance_schema/host_cache","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"host_cache","FullyQualifiedName":"localhost/performance_schema/host_cache","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"host_summary","FullyQualifiedName":"localhost/sys/host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"host_summary","FullyQualifiedName":"localhost/sys/host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"host_summary","FullyQualifiedName":"localhost/sys/host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"host_summary","FullyQualifiedName":"localhost/sys/host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"host_summary","FullyQualifiedName":"localhost/sys/host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"host_summary","FullyQualifiedName":"localhost/sys/host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"host_summary","FullyQualifiedName":"localhost/sys/host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"host_summary","FullyQualifiedName":"localhost/sys/host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"host_summary","FullyQualifiedName":"localhost/sys/host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"host_summary","FullyQualifiedName":"localhost/sys/host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"host_summary","FullyQualifiedName":"localhost/sys/host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"host_summary","FullyQualifiedName":"localhost/sys/host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io","FullyQualifiedName":"localhost/sys/host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io","FullyQualifiedName":"localhost/sys/host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io","FullyQualifiedName":"localhost/sys/host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io","FullyQualifiedName":"localhost/sys/host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io","FullyQualifiedName":"localhost/sys/host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io","FullyQualifiedName":"localhost/sys/host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io","FullyQualifiedName":"localhost/sys/host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io","FullyQualifiedName":"localhost/sys/host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io","FullyQualifiedName":"localhost/sys/host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io","FullyQualifiedName":"localhost/sys/host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io","FullyQualifiedName":"localhost/sys/host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io","FullyQualifiedName":"localhost/sys/host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"host_summary_by_stages","FullyQualifiedName":"localhost/sys/host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"host_summary_by_stages","FullyQualifiedName":"localhost/sys/host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"host_summary_by_stages","FullyQualifiedName":"localhost/sys/host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"host_summary_by_stages","FullyQualifiedName":"localhost/sys/host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"host_summary_by_stages","FullyQualifiedName":"localhost/sys/host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"host_summary_by_stages","FullyQualifiedName":"localhost/sys/host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"host_summary_by_stages","FullyQualifiedName":"localhost/sys/host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"host_summary_by_stages","FullyQualifiedName":"localhost/sys/host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"host_summary_by_stages","FullyQualifiedName":"localhost/sys/host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"host_summary_by_stages","FullyQualifiedName":"localhost/sys/host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"host_summary_by_stages","FullyQualifiedName":"localhost/sys/host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"host_summary_by_stages","FullyQualifiedName":"localhost/sys/host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"hosts","FullyQualifiedName":"localhost/performance_schema/hosts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"hosts","FullyQualifiedName":"localhost/performance_schema/hosts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"hosts","FullyQualifiedName":"localhost/performance_schema/hosts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"hosts","FullyQualifiedName":"localhost/performance_schema/hosts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"hosts","FullyQualifiedName":"localhost/performance_schema/hosts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"hosts","FullyQualifiedName":"localhost/performance_schema/hosts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"hosts","FullyQualifiedName":"localhost/performance_schema/hosts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"hosts","FullyQualifiedName":"localhost/performance_schema/hosts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"hosts","FullyQualifiedName":"localhost/performance_schema/hosts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"hosts","FullyQualifiedName":"localhost/performance_schema/hosts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"hosts","FullyQualifiedName":"localhost/performance_schema/hosts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"hosts","FullyQualifiedName":"localhost/performance_schema/hosts","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"innodb_index_stats","FullyQualifiedName":"localhost/mysql/innodb_index_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"innodb_index_stats","FullyQualifiedName":"localhost/mysql/innodb_index_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"innodb_index_stats","FullyQualifiedName":"localhost/mysql/innodb_index_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"innodb_index_stats","FullyQualifiedName":"localhost/mysql/innodb_index_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"innodb_index_stats","FullyQualifiedName":"localhost/mysql/innodb_index_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"innodb_index_stats","FullyQualifiedName":"localhost/mysql/innodb_index_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"innodb_index_stats","FullyQualifiedName":"localhost/mysql/innodb_index_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"innodb_index_stats","FullyQualifiedName":"localhost/mysql/innodb_index_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"innodb_index_stats","FullyQualifiedName":"localhost/mysql/innodb_index_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"innodb_index_stats","FullyQualifiedName":"localhost/mysql/innodb_index_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"innodb_index_stats","FullyQualifiedName":"localhost/mysql/innodb_index_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"innodb_index_stats","FullyQualifiedName":"localhost/mysql/innodb_index_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"innodb_lock_waits","FullyQualifiedName":"localhost/sys/innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"innodb_lock_waits","FullyQualifiedName":"localhost/sys/innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"innodb_lock_waits","FullyQualifiedName":"localhost/sys/innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"innodb_lock_waits","FullyQualifiedName":"localhost/sys/innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"innodb_lock_waits","FullyQualifiedName":"localhost/sys/innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"innodb_lock_waits","FullyQualifiedName":"localhost/sys/innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"innodb_lock_waits","FullyQualifiedName":"localhost/sys/innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"innodb_lock_waits","FullyQualifiedName":"localhost/sys/innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"innodb_lock_waits","FullyQualifiedName":"localhost/sys/innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"innodb_lock_waits","FullyQualifiedName":"localhost/sys/innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"innodb_lock_waits","FullyQualifiedName":"localhost/sys/innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"innodb_lock_waits","FullyQualifiedName":"localhost/sys/innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"innodb_redo_log_files","FullyQualifiedName":"localhost/performance_schema/innodb_redo_log_files","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"innodb_redo_log_files","FullyQualifiedName":"localhost/performance_schema/innodb_redo_log_files","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"innodb_redo_log_files","FullyQualifiedName":"localhost/performance_schema/innodb_redo_log_files","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"innodb_redo_log_files","FullyQualifiedName":"localhost/performance_schema/innodb_redo_log_files","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"innodb_redo_log_files","FullyQualifiedName":"localhost/performance_schema/innodb_redo_log_files","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"innodb_redo_log_files","FullyQualifiedName":"localhost/performance_schema/innodb_redo_log_files","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"innodb_redo_log_files","FullyQualifiedName":"localhost/performance_schema/innodb_redo_log_files","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"innodb_redo_log_files","FullyQualifiedName":"localhost/performance_schema/innodb_redo_log_files","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"innodb_redo_log_files","FullyQualifiedName":"localhost/performance_schema/innodb_redo_log_files","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"innodb_redo_log_files","FullyQualifiedName":"localhost/performance_schema/innodb_redo_log_files","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"innodb_redo_log_files","FullyQualifiedName":"localhost/performance_schema/innodb_redo_log_files","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"innodb_redo_log_files","FullyQualifiedName":"localhost/performance_schema/innodb_redo_log_files","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"innodb_table_stats","FullyQualifiedName":"localhost/mysql/innodb_table_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"innodb_table_stats","FullyQualifiedName":"localhost/mysql/innodb_table_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"innodb_table_stats","FullyQualifiedName":"localhost/mysql/innodb_table_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"innodb_table_stats","FullyQualifiedName":"localhost/mysql/innodb_table_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"innodb_table_stats","FullyQualifiedName":"localhost/mysql/innodb_table_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"innodb_table_stats","FullyQualifiedName":"localhost/mysql/innodb_table_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"innodb_table_stats","FullyQualifiedName":"localhost/mysql/innodb_table_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"innodb_table_stats","FullyQualifiedName":"localhost/mysql/innodb_table_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"innodb_table_stats","FullyQualifiedName":"localhost/mysql/innodb_table_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"innodb_table_stats","FullyQualifiedName":"localhost/mysql/innodb_table_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"innodb_table_stats","FullyQualifiedName":"localhost/mysql/innodb_table_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"innodb_table_stats","FullyQualifiedName":"localhost/mysql/innodb_table_stats","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"keyring_component_status","FullyQualifiedName":"localhost/performance_schema/keyring_component_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"keyring_component_status","FullyQualifiedName":"localhost/performance_schema/keyring_component_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"keyring_component_status","FullyQualifiedName":"localhost/performance_schema/keyring_component_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"keyring_component_status","FullyQualifiedName":"localhost/performance_schema/keyring_component_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"keyring_component_status","FullyQualifiedName":"localhost/performance_schema/keyring_component_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"keyring_component_status","FullyQualifiedName":"localhost/performance_schema/keyring_component_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"keyring_component_status","FullyQualifiedName":"localhost/performance_schema/keyring_component_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"keyring_component_status","FullyQualifiedName":"localhost/performance_schema/keyring_component_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"keyring_component_status","FullyQualifiedName":"localhost/performance_schema/keyring_component_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"keyring_component_status","FullyQualifiedName":"localhost/performance_schema/keyring_component_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"keyring_component_status","FullyQualifiedName":"localhost/performance_schema/keyring_component_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"keyring_component_status","FullyQualifiedName":"localhost/performance_schema/keyring_component_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"keyring_keys","FullyQualifiedName":"localhost/performance_schema/keyring_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"keyring_keys","FullyQualifiedName":"localhost/performance_schema/keyring_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"keyring_keys","FullyQualifiedName":"localhost/performance_schema/keyring_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"keyring_keys","FullyQualifiedName":"localhost/performance_schema/keyring_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"keyring_keys","FullyQualifiedName":"localhost/performance_schema/keyring_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"keyring_keys","FullyQualifiedName":"localhost/performance_schema/keyring_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"keyring_keys","FullyQualifiedName":"localhost/performance_schema/keyring_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"keyring_keys","FullyQualifiedName":"localhost/performance_schema/keyring_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"keyring_keys","FullyQualifiedName":"localhost/performance_schema/keyring_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"keyring_keys","FullyQualifiedName":"localhost/performance_schema/keyring_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"keyring_keys","FullyQualifiedName":"localhost/performance_schema/keyring_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"keyring_keys","FullyQualifiedName":"localhost/performance_schema/keyring_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"latest_file_io","FullyQualifiedName":"localhost/sys/latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"latest_file_io","FullyQualifiedName":"localhost/sys/latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"latest_file_io","FullyQualifiedName":"localhost/sys/latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"latest_file_io","FullyQualifiedName":"localhost/sys/latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"latest_file_io","FullyQualifiedName":"localhost/sys/latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"latest_file_io","FullyQualifiedName":"localhost/sys/latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"latest_file_io","FullyQualifiedName":"localhost/sys/latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"latest_file_io","FullyQualifiedName":"localhost/sys/latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"latest_file_io","FullyQualifiedName":"localhost/sys/latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"latest_file_io","FullyQualifiedName":"localhost/sys/latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"latest_file_io","FullyQualifiedName":"localhost/sys/latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"latest_file_io","FullyQualifiedName":"localhost/sys/latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"list_add","FullyQualifiedName":"localhost/sys/list_add","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"list_add","FullyQualifiedName":"localhost/sys/list_add","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"list_drop","FullyQualifiedName":"localhost/sys/list_drop","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"list_drop","FullyQualifiedName":"localhost/sys/list_drop","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"log_status","FullyQualifiedName":"localhost/performance_schema/log_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"log_status","FullyQualifiedName":"localhost/performance_schema/log_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"log_status","FullyQualifiedName":"localhost/performance_schema/log_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"log_status","FullyQualifiedName":"localhost/performance_schema/log_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"log_status","FullyQualifiedName":"localhost/performance_schema/log_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"log_status","FullyQualifiedName":"localhost/performance_schema/log_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"log_status","FullyQualifiedName":"localhost/performance_schema/log_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"log_status","FullyQualifiedName":"localhost/performance_schema/log_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"log_status","FullyQualifiedName":"localhost/performance_schema/log_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"log_status","FullyQualifiedName":"localhost/performance_schema/log_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"log_status","FullyQualifiedName":"localhost/performance_schema/log_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"log_status","FullyQualifiedName":"localhost/performance_schema/log_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"memory_global_total","FullyQualifiedName":"localhost/sys/memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"memory_global_total","FullyQualifiedName":"localhost/sys/memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"memory_global_total","FullyQualifiedName":"localhost/sys/memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"memory_global_total","FullyQualifiedName":"localhost/sys/memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"memory_global_total","FullyQualifiedName":"localhost/sys/memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"memory_global_total","FullyQualifiedName":"localhost/sys/memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"memory_global_total","FullyQualifiedName":"localhost/sys/memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"memory_global_total","FullyQualifiedName":"localhost/sys/memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"memory_global_total","FullyQualifiedName":"localhost/sys/memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"memory_global_total","FullyQualifiedName":"localhost/sys/memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"memory_global_total","FullyQualifiedName":"localhost/sys/memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"memory_global_total","FullyQualifiedName":"localhost/sys/memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"memory_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"memory_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"memory_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"memory_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"memory_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"memory_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"memory_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"memory_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"memory_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"memory_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"memory_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"memory_summary_by_account_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_account_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"memory_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"memory_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"memory_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"memory_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"memory_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"memory_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"memory_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"memory_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"memory_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"memory_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"memory_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"memory_summary_by_host_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_host_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"memory_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"memory_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"memory_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"memory_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"memory_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"memory_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"memory_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"memory_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"memory_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"memory_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"memory_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"memory_summary_by_thread_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_thread_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"memory_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"memory_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"memory_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"memory_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"memory_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"memory_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"memory_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"memory_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"memory_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"memory_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"memory_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"memory_summary_by_user_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_by_user_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"memory_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"memory_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"memory_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"memory_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"memory_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"memory_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"memory_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"memory_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"memory_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"memory_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"memory_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"memory_summary_global_by_event_name","FullyQualifiedName":"localhost/performance_schema/memory_summary_global_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"metadata_locks","FullyQualifiedName":"localhost/performance_schema/metadata_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"metadata_locks","FullyQualifiedName":"localhost/performance_schema/metadata_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"metadata_locks","FullyQualifiedName":"localhost/performance_schema/metadata_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"metadata_locks","FullyQualifiedName":"localhost/performance_schema/metadata_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"metadata_locks","FullyQualifiedName":"localhost/performance_schema/metadata_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"metadata_locks","FullyQualifiedName":"localhost/performance_schema/metadata_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"metadata_locks","FullyQualifiedName":"localhost/performance_schema/metadata_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"metadata_locks","FullyQualifiedName":"localhost/performance_schema/metadata_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"metadata_locks","FullyQualifiedName":"localhost/performance_schema/metadata_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"metadata_locks","FullyQualifiedName":"localhost/performance_schema/metadata_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"metadata_locks","FullyQualifiedName":"localhost/performance_schema/metadata_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"metadata_locks","FullyQualifiedName":"localhost/performance_schema/metadata_locks","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"metrics","FullyQualifiedName":"localhost/sys/metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"metrics","FullyQualifiedName":"localhost/sys/metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"metrics","FullyQualifiedName":"localhost/sys/metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"metrics","FullyQualifiedName":"localhost/sys/metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"metrics","FullyQualifiedName":"localhost/sys/metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"metrics","FullyQualifiedName":"localhost/sys/metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"metrics","FullyQualifiedName":"localhost/sys/metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"metrics","FullyQualifiedName":"localhost/sys/metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"metrics","FullyQualifiedName":"localhost/sys/metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"metrics","FullyQualifiedName":"localhost/sys/metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"metrics","FullyQualifiedName":"localhost/sys/metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"metrics","FullyQualifiedName":"localhost/sys/metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"mutex_instances","FullyQualifiedName":"localhost/performance_schema/mutex_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"mutex_instances","FullyQualifiedName":"localhost/performance_schema/mutex_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"mutex_instances","FullyQualifiedName":"localhost/performance_schema/mutex_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"mutex_instances","FullyQualifiedName":"localhost/performance_schema/mutex_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"mutex_instances","FullyQualifiedName":"localhost/performance_schema/mutex_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"mutex_instances","FullyQualifiedName":"localhost/performance_schema/mutex_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"mutex_instances","FullyQualifiedName":"localhost/performance_schema/mutex_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"mutex_instances","FullyQualifiedName":"localhost/performance_schema/mutex_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"mutex_instances","FullyQualifiedName":"localhost/performance_schema/mutex_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"mutex_instances","FullyQualifiedName":"localhost/performance_schema/mutex_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"mutex_instances","FullyQualifiedName":"localhost/performance_schema/mutex_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"mutex_instances","FullyQualifiedName":"localhost/performance_schema/mutex_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"CREATE ROUTINE","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"CREATE TEMPORARY TABLES","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"EVENT","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"LOCK TABLES","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"ndb_binlog_index","FullyQualifiedName":"localhost/mysql/ndb_binlog_index","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"ndb_binlog_index","FullyQualifiedName":"localhost/mysql/ndb_binlog_index","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"ndb_binlog_index","FullyQualifiedName":"localhost/mysql/ndb_binlog_index","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"ndb_binlog_index","FullyQualifiedName":"localhost/mysql/ndb_binlog_index","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"ndb_binlog_index","FullyQualifiedName":"localhost/mysql/ndb_binlog_index","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"ndb_binlog_index","FullyQualifiedName":"localhost/mysql/ndb_binlog_index","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"ndb_binlog_index","FullyQualifiedName":"localhost/mysql/ndb_binlog_index","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"ndb_binlog_index","FullyQualifiedName":"localhost/mysql/ndb_binlog_index","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"ndb_binlog_index","FullyQualifiedName":"localhost/mysql/ndb_binlog_index","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"ndb_binlog_index","FullyQualifiedName":"localhost/mysql/ndb_binlog_index","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"ndb_binlog_index","FullyQualifiedName":"localhost/mysql/ndb_binlog_index","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"ndb_binlog_index","FullyQualifiedName":"localhost/mysql/ndb_binlog_index","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"objects_summary_global_by_type","FullyQualifiedName":"localhost/performance_schema/objects_summary_global_by_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"objects_summary_global_by_type","FullyQualifiedName":"localhost/performance_schema/objects_summary_global_by_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"objects_summary_global_by_type","FullyQualifiedName":"localhost/performance_schema/objects_summary_global_by_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"objects_summary_global_by_type","FullyQualifiedName":"localhost/performance_schema/objects_summary_global_by_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"objects_summary_global_by_type","FullyQualifiedName":"localhost/performance_schema/objects_summary_global_by_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"objects_summary_global_by_type","FullyQualifiedName":"localhost/performance_schema/objects_summary_global_by_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"objects_summary_global_by_type","FullyQualifiedName":"localhost/performance_schema/objects_summary_global_by_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"objects_summary_global_by_type","FullyQualifiedName":"localhost/performance_schema/objects_summary_global_by_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"objects_summary_global_by_type","FullyQualifiedName":"localhost/performance_schema/objects_summary_global_by_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"objects_summary_global_by_type","FullyQualifiedName":"localhost/performance_schema/objects_summary_global_by_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"objects_summary_global_by_type","FullyQualifiedName":"localhost/performance_schema/objects_summary_global_by_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"objects_summary_global_by_type","FullyQualifiedName":"localhost/performance_schema/objects_summary_global_by_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"password_history","FullyQualifiedName":"localhost/mysql/password_history","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"password_history","FullyQualifiedName":"localhost/mysql/password_history","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"password_history","FullyQualifiedName":"localhost/mysql/password_history","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"password_history","FullyQualifiedName":"localhost/mysql/password_history","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"password_history","FullyQualifiedName":"localhost/mysql/password_history","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"password_history","FullyQualifiedName":"localhost/mysql/password_history","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"password_history","FullyQualifiedName":"localhost/mysql/password_history","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"password_history","FullyQualifiedName":"localhost/mysql/password_history","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"password_history","FullyQualifiedName":"localhost/mysql/password_history","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"password_history","FullyQualifiedName":"localhost/mysql/password_history","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"password_history","FullyQualifiedName":"localhost/mysql/password_history","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"password_history","FullyQualifiedName":"localhost/mysql/password_history","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"performance_timers","FullyQualifiedName":"localhost/performance_schema/performance_timers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"performance_timers","FullyQualifiedName":"localhost/performance_schema/performance_timers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"performance_timers","FullyQualifiedName":"localhost/performance_schema/performance_timers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"performance_timers","FullyQualifiedName":"localhost/performance_schema/performance_timers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"performance_timers","FullyQualifiedName":"localhost/performance_schema/performance_timers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"performance_timers","FullyQualifiedName":"localhost/performance_schema/performance_timers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"performance_timers","FullyQualifiedName":"localhost/performance_schema/performance_timers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"performance_timers","FullyQualifiedName":"localhost/performance_schema/performance_timers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"performance_timers","FullyQualifiedName":"localhost/performance_schema/performance_timers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"performance_timers","FullyQualifiedName":"localhost/performance_schema/performance_timers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"performance_timers","FullyQualifiedName":"localhost/performance_schema/performance_timers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"performance_timers","FullyQualifiedName":"localhost/performance_schema/performance_timers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"persisted_variables","FullyQualifiedName":"localhost/performance_schema/persisted_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"persisted_variables","FullyQualifiedName":"localhost/performance_schema/persisted_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"persisted_variables","FullyQualifiedName":"localhost/performance_schema/persisted_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"persisted_variables","FullyQualifiedName":"localhost/performance_schema/persisted_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"persisted_variables","FullyQualifiedName":"localhost/performance_schema/persisted_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"persisted_variables","FullyQualifiedName":"localhost/performance_schema/persisted_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"persisted_variables","FullyQualifiedName":"localhost/performance_schema/persisted_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"persisted_variables","FullyQualifiedName":"localhost/performance_schema/persisted_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"persisted_variables","FullyQualifiedName":"localhost/performance_schema/persisted_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"persisted_variables","FullyQualifiedName":"localhost/performance_schema/persisted_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"persisted_variables","FullyQualifiedName":"localhost/performance_schema/persisted_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"persisted_variables","FullyQualifiedName":"localhost/performance_schema/persisted_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"plugin","FullyQualifiedName":"localhost/mysql/plugin","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"plugin","FullyQualifiedName":"localhost/mysql/plugin","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"plugin","FullyQualifiedName":"localhost/mysql/plugin","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"plugin","FullyQualifiedName":"localhost/mysql/plugin","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"plugin","FullyQualifiedName":"localhost/mysql/plugin","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"plugin","FullyQualifiedName":"localhost/mysql/plugin","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"plugin","FullyQualifiedName":"localhost/mysql/plugin","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"plugin","FullyQualifiedName":"localhost/mysql/plugin","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"plugin","FullyQualifiedName":"localhost/mysql/plugin","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"plugin","FullyQualifiedName":"localhost/mysql/plugin","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"plugin","FullyQualifiedName":"localhost/mysql/plugin","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"plugin","FullyQualifiedName":"localhost/mysql/plugin","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"prepared_statements_instances","FullyQualifiedName":"localhost/performance_schema/prepared_statements_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"prepared_statements_instances","FullyQualifiedName":"localhost/performance_schema/prepared_statements_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"prepared_statements_instances","FullyQualifiedName":"localhost/performance_schema/prepared_statements_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"prepared_statements_instances","FullyQualifiedName":"localhost/performance_schema/prepared_statements_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"prepared_statements_instances","FullyQualifiedName":"localhost/performance_schema/prepared_statements_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"prepared_statements_instances","FullyQualifiedName":"localhost/performance_schema/prepared_statements_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"prepared_statements_instances","FullyQualifiedName":"localhost/performance_schema/prepared_statements_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"prepared_statements_instances","FullyQualifiedName":"localhost/performance_schema/prepared_statements_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"prepared_statements_instances","FullyQualifiedName":"localhost/performance_schema/prepared_statements_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"prepared_statements_instances","FullyQualifiedName":"localhost/performance_schema/prepared_statements_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"prepared_statements_instances","FullyQualifiedName":"localhost/performance_schema/prepared_statements_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"prepared_statements_instances","FullyQualifiedName":"localhost/performance_schema/prepared_statements_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/sys/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/performance_schema/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/sys/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/performance_schema/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/sys/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/performance_schema/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/sys/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/performance_schema/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/sys/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/performance_schema/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/sys/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/performance_schema/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/sys/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/performance_schema/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/sys/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/performance_schema/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/sys/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/performance_schema/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/sys/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/performance_schema/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/sys/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/performance_schema/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/sys/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"processlist","FullyQualifiedName":"localhost/performance_schema/processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"procs_priv","FullyQualifiedName":"localhost/mysql/procs_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"procs_priv","FullyQualifiedName":"localhost/mysql/procs_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"procs_priv","FullyQualifiedName":"localhost/mysql/procs_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"procs_priv","FullyQualifiedName":"localhost/mysql/procs_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"procs_priv","FullyQualifiedName":"localhost/mysql/procs_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"procs_priv","FullyQualifiedName":"localhost/mysql/procs_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"procs_priv","FullyQualifiedName":"localhost/mysql/procs_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"procs_priv","FullyQualifiedName":"localhost/mysql/procs_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"procs_priv","FullyQualifiedName":"localhost/mysql/procs_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"procs_priv","FullyQualifiedName":"localhost/mysql/procs_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"procs_priv","FullyQualifiedName":"localhost/mysql/procs_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"procs_priv","FullyQualifiedName":"localhost/mysql/procs_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"proxies_priv","FullyQualifiedName":"localhost/mysql/proxies_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"proxies_priv","FullyQualifiedName":"localhost/mysql/proxies_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"proxies_priv","FullyQualifiedName":"localhost/mysql/proxies_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"proxies_priv","FullyQualifiedName":"localhost/mysql/proxies_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"proxies_priv","FullyQualifiedName":"localhost/mysql/proxies_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"proxies_priv","FullyQualifiedName":"localhost/mysql/proxies_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"proxies_priv","FullyQualifiedName":"localhost/mysql/proxies_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"proxies_priv","FullyQualifiedName":"localhost/mysql/proxies_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"proxies_priv","FullyQualifiedName":"localhost/mysql/proxies_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"proxies_priv","FullyQualifiedName":"localhost/mysql/proxies_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"proxies_priv","FullyQualifiedName":"localhost/mysql/proxies_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"proxies_priv","FullyQualifiedName":"localhost/mysql/proxies_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"ps_check_lost_instrumentation","FullyQualifiedName":"localhost/sys/ps_check_lost_instrumentation","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"ps_check_lost_instrumentation","FullyQualifiedName":"localhost/sys/ps_check_lost_instrumentation","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"ps_check_lost_instrumentation","FullyQualifiedName":"localhost/sys/ps_check_lost_instrumentation","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"ps_check_lost_instrumentation","FullyQualifiedName":"localhost/sys/ps_check_lost_instrumentation","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"ps_check_lost_instrumentation","FullyQualifiedName":"localhost/sys/ps_check_lost_instrumentation","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"ps_check_lost_instrumentation","FullyQualifiedName":"localhost/sys/ps_check_lost_instrumentation","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"ps_check_lost_instrumentation","FullyQualifiedName":"localhost/sys/ps_check_lost_instrumentation","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"ps_check_lost_instrumentation","FullyQualifiedName":"localhost/sys/ps_check_lost_instrumentation","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"ps_check_lost_instrumentation","FullyQualifiedName":"localhost/sys/ps_check_lost_instrumentation","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"ps_check_lost_instrumentation","FullyQualifiedName":"localhost/sys/ps_check_lost_instrumentation","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"ps_check_lost_instrumentation","FullyQualifiedName":"localhost/sys/ps_check_lost_instrumentation","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"ps_check_lost_instrumentation","FullyQualifiedName":"localhost/sys/ps_check_lost_instrumentation","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"ps_is_account_enabled","FullyQualifiedName":"localhost/sys/ps_is_account_enabled","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_is_account_enabled","FullyQualifiedName":"localhost/sys/ps_is_account_enabled","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_is_consumer_enabled","FullyQualifiedName":"localhost/sys/ps_is_consumer_enabled","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_is_consumer_enabled","FullyQualifiedName":"localhost/sys/ps_is_consumer_enabled","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_is_instrument_default_enabled","FullyQualifiedName":"localhost/sys/ps_is_instrument_default_enabled","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_is_instrument_default_enabled","FullyQualifiedName":"localhost/sys/ps_is_instrument_default_enabled","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_is_instrument_default_timed","FullyQualifiedName":"localhost/sys/ps_is_instrument_default_timed","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_is_instrument_default_timed","FullyQualifiedName":"localhost/sys/ps_is_instrument_default_timed","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_is_thread_instrumented","FullyQualifiedName":"localhost/sys/ps_is_thread_instrumented","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_is_thread_instrumented","FullyQualifiedName":"localhost/sys/ps_is_thread_instrumented","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_disable_background_threads","FullyQualifiedName":"localhost/sys/ps_setup_disable_background_threads","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_disable_background_threads","FullyQualifiedName":"localhost/sys/ps_setup_disable_background_threads","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_disable_consumer","FullyQualifiedName":"localhost/sys/ps_setup_disable_consumer","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_disable_consumer","FullyQualifiedName":"localhost/sys/ps_setup_disable_consumer","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_disable_instrument","FullyQualifiedName":"localhost/sys/ps_setup_disable_instrument","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_disable_instrument","FullyQualifiedName":"localhost/sys/ps_setup_disable_instrument","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_disable_thread","FullyQualifiedName":"localhost/sys/ps_setup_disable_thread","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_disable_thread","FullyQualifiedName":"localhost/sys/ps_setup_disable_thread","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_enable_background_threads","FullyQualifiedName":"localhost/sys/ps_setup_enable_background_threads","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_enable_background_threads","FullyQualifiedName":"localhost/sys/ps_setup_enable_background_threads","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_enable_consumer","FullyQualifiedName":"localhost/sys/ps_setup_enable_consumer","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_enable_consumer","FullyQualifiedName":"localhost/sys/ps_setup_enable_consumer","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_enable_instrument","FullyQualifiedName":"localhost/sys/ps_setup_enable_instrument","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_enable_instrument","FullyQualifiedName":"localhost/sys/ps_setup_enable_instrument","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_enable_thread","FullyQualifiedName":"localhost/sys/ps_setup_enable_thread","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_enable_thread","FullyQualifiedName":"localhost/sys/ps_setup_enable_thread","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_reload_saved","FullyQualifiedName":"localhost/sys/ps_setup_reload_saved","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_reload_saved","FullyQualifiedName":"localhost/sys/ps_setup_reload_saved","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_reset_to_default","FullyQualifiedName":"localhost/sys/ps_setup_reset_to_default","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_reset_to_default","FullyQualifiedName":"localhost/sys/ps_setup_reset_to_default","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_save","FullyQualifiedName":"localhost/sys/ps_setup_save","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_save","FullyQualifiedName":"localhost/sys/ps_setup_save","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_show_disabled","FullyQualifiedName":"localhost/sys/ps_setup_show_disabled","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_show_disabled","FullyQualifiedName":"localhost/sys/ps_setup_show_disabled","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_show_disabled_consumers","FullyQualifiedName":"localhost/sys/ps_setup_show_disabled_consumers","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_show_disabled_consumers","FullyQualifiedName":"localhost/sys/ps_setup_show_disabled_consumers","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_show_disabled_instruments","FullyQualifiedName":"localhost/sys/ps_setup_show_disabled_instruments","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_show_disabled_instruments","FullyQualifiedName":"localhost/sys/ps_setup_show_disabled_instruments","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_show_enabled","FullyQualifiedName":"localhost/sys/ps_setup_show_enabled","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_show_enabled","FullyQualifiedName":"localhost/sys/ps_setup_show_enabled","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_show_enabled_consumers","FullyQualifiedName":"localhost/sys/ps_setup_show_enabled_consumers","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_show_enabled_consumers","FullyQualifiedName":"localhost/sys/ps_setup_show_enabled_consumers","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_setup_show_enabled_instruments","FullyQualifiedName":"localhost/sys/ps_setup_show_enabled_instruments","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_setup_show_enabled_instruments","FullyQualifiedName":"localhost/sys/ps_setup_show_enabled_instruments","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_statement_avg_latency_histogram","FullyQualifiedName":"localhost/sys/ps_statement_avg_latency_histogram","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_statement_avg_latency_histogram","FullyQualifiedName":"localhost/sys/ps_statement_avg_latency_histogram","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_thread_account","FullyQualifiedName":"localhost/sys/ps_thread_account","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_thread_account","FullyQualifiedName":"localhost/sys/ps_thread_account","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_thread_id","FullyQualifiedName":"localhost/sys/ps_thread_id","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_thread_id","FullyQualifiedName":"localhost/sys/ps_thread_id","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_thread_stack","FullyQualifiedName":"localhost/sys/ps_thread_stack","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_thread_stack","FullyQualifiedName":"localhost/sys/ps_thread_stack","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_thread_trx_info","FullyQualifiedName":"localhost/sys/ps_thread_trx_info","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_thread_trx_info","FullyQualifiedName":"localhost/sys/ps_thread_trx_info","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_trace_statement_digest","FullyQualifiedName":"localhost/sys/ps_trace_statement_digest","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_trace_statement_digest","FullyQualifiedName":"localhost/sys/ps_trace_statement_digest","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_trace_thread","FullyQualifiedName":"localhost/sys/ps_trace_thread","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_trace_thread","FullyQualifiedName":"localhost/sys/ps_trace_thread","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"ps_truncate_all_tables","FullyQualifiedName":"localhost/sys/ps_truncate_all_tables","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"ps_truncate_all_tables","FullyQualifiedName":"localhost/sys/ps_truncate_all_tables","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"quote_identifier","FullyQualifiedName":"localhost/sys/quote_identifier","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"quote_identifier","FullyQualifiedName":"localhost/sys/quote_identifier","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"replication_applier_configuration","FullyQualifiedName":"localhost/performance_schema/replication_applier_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_applier_configuration","FullyQualifiedName":"localhost/performance_schema/replication_applier_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_applier_configuration","FullyQualifiedName":"localhost/performance_schema/replication_applier_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_applier_configuration","FullyQualifiedName":"localhost/performance_schema/replication_applier_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_applier_configuration","FullyQualifiedName":"localhost/performance_schema/replication_applier_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_applier_configuration","FullyQualifiedName":"localhost/performance_schema/replication_applier_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_applier_configuration","FullyQualifiedName":"localhost/performance_schema/replication_applier_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_applier_configuration","FullyQualifiedName":"localhost/performance_schema/replication_applier_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_applier_configuration","FullyQualifiedName":"localhost/performance_schema/replication_applier_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_applier_configuration","FullyQualifiedName":"localhost/performance_schema/replication_applier_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_applier_configuration","FullyQualifiedName":"localhost/performance_schema/replication_applier_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_applier_configuration","FullyQualifiedName":"localhost/performance_schema/replication_applier_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"replication_applier_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_applier_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_applier_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_applier_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_applier_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_applier_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_applier_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_applier_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_applier_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_applier_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_applier_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_applier_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"replication_applier_global_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_global_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_applier_global_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_global_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_applier_global_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_global_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_applier_global_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_global_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_applier_global_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_global_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_applier_global_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_global_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_applier_global_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_global_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_applier_global_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_global_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_applier_global_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_global_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_applier_global_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_global_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_applier_global_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_global_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_applier_global_filters","FullyQualifiedName":"localhost/performance_schema/replication_applier_global_filters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"replication_applier_status","FullyQualifiedName":"localhost/performance_schema/replication_applier_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_applier_status","FullyQualifiedName":"localhost/performance_schema/replication_applier_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_applier_status","FullyQualifiedName":"localhost/performance_schema/replication_applier_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_applier_status","FullyQualifiedName":"localhost/performance_schema/replication_applier_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_applier_status","FullyQualifiedName":"localhost/performance_schema/replication_applier_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_applier_status","FullyQualifiedName":"localhost/performance_schema/replication_applier_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_applier_status","FullyQualifiedName":"localhost/performance_schema/replication_applier_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_applier_status","FullyQualifiedName":"localhost/performance_schema/replication_applier_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_applier_status","FullyQualifiedName":"localhost/performance_schema/replication_applier_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_applier_status","FullyQualifiedName":"localhost/performance_schema/replication_applier_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_applier_status","FullyQualifiedName":"localhost/performance_schema/replication_applier_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_applier_status","FullyQualifiedName":"localhost/performance_schema/replication_applier_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_coordinator","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_coordinator","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_coordinator","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_coordinator","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_coordinator","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_coordinator","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_coordinator","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_coordinator","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_coordinator","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_coordinator","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_coordinator","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_coordinator","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_coordinator","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_coordinator","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_coordinator","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_coordinator","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_coordinator","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_coordinator","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_coordinator","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_coordinator","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_coordinator","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_coordinator","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_coordinator","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_coordinator","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_worker","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_worker","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_worker","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_worker","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_worker","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_worker","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_worker","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_worker","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_worker","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_worker","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_worker","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_worker","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_worker","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_worker","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_worker","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_worker","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_worker","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_worker","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_worker","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_worker","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_worker","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_worker","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_applier_status_by_worker","FullyQualifiedName":"localhost/performance_schema/replication_applier_status_by_worker","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/mysql/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"replication_asynchronous_connection_failover_managed","FullyQualifiedName":"localhost/performance_schema/replication_asynchronous_connection_failover_managed","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"replication_connection_configuration","FullyQualifiedName":"localhost/performance_schema/replication_connection_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_connection_configuration","FullyQualifiedName":"localhost/performance_schema/replication_connection_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_connection_configuration","FullyQualifiedName":"localhost/performance_schema/replication_connection_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_connection_configuration","FullyQualifiedName":"localhost/performance_schema/replication_connection_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_connection_configuration","FullyQualifiedName":"localhost/performance_schema/replication_connection_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_connection_configuration","FullyQualifiedName":"localhost/performance_schema/replication_connection_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_connection_configuration","FullyQualifiedName":"localhost/performance_schema/replication_connection_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_connection_configuration","FullyQualifiedName":"localhost/performance_schema/replication_connection_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_connection_configuration","FullyQualifiedName":"localhost/performance_schema/replication_connection_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_connection_configuration","FullyQualifiedName":"localhost/performance_schema/replication_connection_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_connection_configuration","FullyQualifiedName":"localhost/performance_schema/replication_connection_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_connection_configuration","FullyQualifiedName":"localhost/performance_schema/replication_connection_configuration","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"replication_connection_status","FullyQualifiedName":"localhost/performance_schema/replication_connection_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_connection_status","FullyQualifiedName":"localhost/performance_schema/replication_connection_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_connection_status","FullyQualifiedName":"localhost/performance_schema/replication_connection_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_connection_status","FullyQualifiedName":"localhost/performance_schema/replication_connection_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_connection_status","FullyQualifiedName":"localhost/performance_schema/replication_connection_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_connection_status","FullyQualifiedName":"localhost/performance_schema/replication_connection_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_connection_status","FullyQualifiedName":"localhost/performance_schema/replication_connection_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_connection_status","FullyQualifiedName":"localhost/performance_schema/replication_connection_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_connection_status","FullyQualifiedName":"localhost/performance_schema/replication_connection_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_connection_status","FullyQualifiedName":"localhost/performance_schema/replication_connection_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_connection_status","FullyQualifiedName":"localhost/performance_schema/replication_connection_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_connection_status","FullyQualifiedName":"localhost/performance_schema/replication_connection_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"replication_group_configuration_version","FullyQualifiedName":"localhost/mysql/replication_group_configuration_version","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_group_configuration_version","FullyQualifiedName":"localhost/mysql/replication_group_configuration_version","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_group_configuration_version","FullyQualifiedName":"localhost/mysql/replication_group_configuration_version","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_group_configuration_version","FullyQualifiedName":"localhost/mysql/replication_group_configuration_version","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_group_configuration_version","FullyQualifiedName":"localhost/mysql/replication_group_configuration_version","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_group_configuration_version","FullyQualifiedName":"localhost/mysql/replication_group_configuration_version","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_group_configuration_version","FullyQualifiedName":"localhost/mysql/replication_group_configuration_version","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_group_configuration_version","FullyQualifiedName":"localhost/mysql/replication_group_configuration_version","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_group_configuration_version","FullyQualifiedName":"localhost/mysql/replication_group_configuration_version","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_group_configuration_version","FullyQualifiedName":"localhost/mysql/replication_group_configuration_version","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_group_configuration_version","FullyQualifiedName":"localhost/mysql/replication_group_configuration_version","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_group_configuration_version","FullyQualifiedName":"localhost/mysql/replication_group_configuration_version","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"replication_group_member_actions","FullyQualifiedName":"localhost/mysql/replication_group_member_actions","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_group_member_actions","FullyQualifiedName":"localhost/mysql/replication_group_member_actions","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_group_member_actions","FullyQualifiedName":"localhost/mysql/replication_group_member_actions","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_group_member_actions","FullyQualifiedName":"localhost/mysql/replication_group_member_actions","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_group_member_actions","FullyQualifiedName":"localhost/mysql/replication_group_member_actions","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_group_member_actions","FullyQualifiedName":"localhost/mysql/replication_group_member_actions","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_group_member_actions","FullyQualifiedName":"localhost/mysql/replication_group_member_actions","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_group_member_actions","FullyQualifiedName":"localhost/mysql/replication_group_member_actions","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_group_member_actions","FullyQualifiedName":"localhost/mysql/replication_group_member_actions","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_group_member_actions","FullyQualifiedName":"localhost/mysql/replication_group_member_actions","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_group_member_actions","FullyQualifiedName":"localhost/mysql/replication_group_member_actions","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_group_member_actions","FullyQualifiedName":"localhost/mysql/replication_group_member_actions","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"replication_group_member_stats","FullyQualifiedName":"localhost/performance_schema/replication_group_member_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_group_member_stats","FullyQualifiedName":"localhost/performance_schema/replication_group_member_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_group_member_stats","FullyQualifiedName":"localhost/performance_schema/replication_group_member_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_group_member_stats","FullyQualifiedName":"localhost/performance_schema/replication_group_member_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_group_member_stats","FullyQualifiedName":"localhost/performance_schema/replication_group_member_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_group_member_stats","FullyQualifiedName":"localhost/performance_schema/replication_group_member_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_group_member_stats","FullyQualifiedName":"localhost/performance_schema/replication_group_member_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_group_member_stats","FullyQualifiedName":"localhost/performance_schema/replication_group_member_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_group_member_stats","FullyQualifiedName":"localhost/performance_schema/replication_group_member_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_group_member_stats","FullyQualifiedName":"localhost/performance_schema/replication_group_member_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_group_member_stats","FullyQualifiedName":"localhost/performance_schema/replication_group_member_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_group_member_stats","FullyQualifiedName":"localhost/performance_schema/replication_group_member_stats","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"replication_group_members","FullyQualifiedName":"localhost/performance_schema/replication_group_members","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"replication_group_members","FullyQualifiedName":"localhost/performance_schema/replication_group_members","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"replication_group_members","FullyQualifiedName":"localhost/performance_schema/replication_group_members","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"replication_group_members","FullyQualifiedName":"localhost/performance_schema/replication_group_members","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"replication_group_members","FullyQualifiedName":"localhost/performance_schema/replication_group_members","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"replication_group_members","FullyQualifiedName":"localhost/performance_schema/replication_group_members","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"replication_group_members","FullyQualifiedName":"localhost/performance_schema/replication_group_members","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"replication_group_members","FullyQualifiedName":"localhost/performance_schema/replication_group_members","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"replication_group_members","FullyQualifiedName":"localhost/performance_schema/replication_group_members","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"replication_group_members","FullyQualifiedName":"localhost/performance_schema/replication_group_members","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"replication_group_members","FullyQualifiedName":"localhost/performance_schema/replication_group_members","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"replication_group_members","FullyQualifiedName":"localhost/performance_schema/replication_group_members","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"role_edges","FullyQualifiedName":"localhost/mysql/role_edges","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"role_edges","FullyQualifiedName":"localhost/mysql/role_edges","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"role_edges","FullyQualifiedName":"localhost/mysql/role_edges","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"role_edges","FullyQualifiedName":"localhost/mysql/role_edges","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"role_edges","FullyQualifiedName":"localhost/mysql/role_edges","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"role_edges","FullyQualifiedName":"localhost/mysql/role_edges","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"role_edges","FullyQualifiedName":"localhost/mysql/role_edges","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"role_edges","FullyQualifiedName":"localhost/mysql/role_edges","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"role_edges","FullyQualifiedName":"localhost/mysql/role_edges","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"role_edges","FullyQualifiedName":"localhost/mysql/role_edges","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"role_edges","FullyQualifiedName":"localhost/mysql/role_edges","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"role_edges","FullyQualifiedName":"localhost/mysql/role_edges","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"ALLOW_NONEXISTENT_DEFINER","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"APPLICATION_PASSWORD_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"AUDIT_ABORT_EXEMPT","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"AUDIT_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"AUTHENTICATION_POLICY_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"BACKUP_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"BINLOG_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"BINLOG_ENCRYPTION_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"CLONE_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"CONNECTION_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"CREATE ROLE","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"CREATE ROUTINE","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"CREATE TABLESPACE","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"CREATE TEMPORARY TABLES","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"CREATE USER","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"DROP ROLE","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"ENCRYPTION_KEY_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"EVENT","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"FILE","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"FIREWALL_EXEMPT","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"FLUSH_OPTIMIZER_COSTS","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"FLUSH_STATUS","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"FLUSH_TABLES","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"FLUSH_USER_RESOURCES","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"GROUP_REPLICATION_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"GROUP_REPLICATION_STREAM","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"INNODB_REDO_LOG_ARCHIVE","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"INNODB_REDO_LOG_ENABLE","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"LOCK TABLES","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"PASSWORDLESS_USER_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"PERSIST_RO_VARIABLES_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"PROCESS","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"RELOAD","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"REPLICATION CLIENT","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"REPLICATION SLAVE","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"REPLICATION_APPLIER","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"REPLICATION_SLAVE_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"RESOURCE_GROUP_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"RESOURCE_GROUP_USER","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"ROLE_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"SENSITIVE_VARIABLES_OBSERVER","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"SERVICE_CONNECTION_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"SESSION_VARIABLES_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"SET_ANY_DEFINER","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"SHOW DATABASES","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"SHOW_ROUTINE","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"SHUTDOWN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"SUPER","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"SYSTEM_USER","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"SYSTEM_VARIABLES_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"TABLE_ENCRYPTION_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"TELEMETRY_LOG_ADMIN","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"TRANSACTION_GTID_TAG","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"XA_RECOVER_ADMIN","Parent":null}},{"Resource":{"Name":"rwlock_instances","FullyQualifiedName":"localhost/performance_schema/rwlock_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"rwlock_instances","FullyQualifiedName":"localhost/performance_schema/rwlock_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"rwlock_instances","FullyQualifiedName":"localhost/performance_schema/rwlock_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"rwlock_instances","FullyQualifiedName":"localhost/performance_schema/rwlock_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"rwlock_instances","FullyQualifiedName":"localhost/performance_schema/rwlock_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"rwlock_instances","FullyQualifiedName":"localhost/performance_schema/rwlock_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"rwlock_instances","FullyQualifiedName":"localhost/performance_schema/rwlock_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"rwlock_instances","FullyQualifiedName":"localhost/performance_schema/rwlock_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"rwlock_instances","FullyQualifiedName":"localhost/performance_schema/rwlock_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"rwlock_instances","FullyQualifiedName":"localhost/performance_schema/rwlock_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"rwlock_instances","FullyQualifiedName":"localhost/performance_schema/rwlock_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"rwlock_instances","FullyQualifiedName":"localhost/performance_schema/rwlock_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"schema_auto_increment_columns","FullyQualifiedName":"localhost/sys/schema_auto_increment_columns","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"schema_auto_increment_columns","FullyQualifiedName":"localhost/sys/schema_auto_increment_columns","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"schema_auto_increment_columns","FullyQualifiedName":"localhost/sys/schema_auto_increment_columns","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"schema_auto_increment_columns","FullyQualifiedName":"localhost/sys/schema_auto_increment_columns","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"schema_auto_increment_columns","FullyQualifiedName":"localhost/sys/schema_auto_increment_columns","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"schema_auto_increment_columns","FullyQualifiedName":"localhost/sys/schema_auto_increment_columns","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"schema_auto_increment_columns","FullyQualifiedName":"localhost/sys/schema_auto_increment_columns","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"schema_auto_increment_columns","FullyQualifiedName":"localhost/sys/schema_auto_increment_columns","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"schema_auto_increment_columns","FullyQualifiedName":"localhost/sys/schema_auto_increment_columns","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"schema_auto_increment_columns","FullyQualifiedName":"localhost/sys/schema_auto_increment_columns","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"schema_auto_increment_columns","FullyQualifiedName":"localhost/sys/schema_auto_increment_columns","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"schema_auto_increment_columns","FullyQualifiedName":"localhost/sys/schema_auto_increment_columns","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"schema_index_statistics","FullyQualifiedName":"localhost/sys/schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"schema_index_statistics","FullyQualifiedName":"localhost/sys/schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"schema_index_statistics","FullyQualifiedName":"localhost/sys/schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"schema_index_statistics","FullyQualifiedName":"localhost/sys/schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"schema_index_statistics","FullyQualifiedName":"localhost/sys/schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"schema_index_statistics","FullyQualifiedName":"localhost/sys/schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"schema_index_statistics","FullyQualifiedName":"localhost/sys/schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"schema_index_statistics","FullyQualifiedName":"localhost/sys/schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"schema_index_statistics","FullyQualifiedName":"localhost/sys/schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"schema_index_statistics","FullyQualifiedName":"localhost/sys/schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"schema_index_statistics","FullyQualifiedName":"localhost/sys/schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"schema_index_statistics","FullyQualifiedName":"localhost/sys/schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"schema_object_overview","FullyQualifiedName":"localhost/sys/schema_object_overview","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"schema_object_overview","FullyQualifiedName":"localhost/sys/schema_object_overview","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"schema_object_overview","FullyQualifiedName":"localhost/sys/schema_object_overview","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"schema_object_overview","FullyQualifiedName":"localhost/sys/schema_object_overview","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"schema_object_overview","FullyQualifiedName":"localhost/sys/schema_object_overview","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"schema_object_overview","FullyQualifiedName":"localhost/sys/schema_object_overview","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"schema_object_overview","FullyQualifiedName":"localhost/sys/schema_object_overview","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"schema_object_overview","FullyQualifiedName":"localhost/sys/schema_object_overview","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"schema_object_overview","FullyQualifiedName":"localhost/sys/schema_object_overview","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"schema_object_overview","FullyQualifiedName":"localhost/sys/schema_object_overview","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"schema_object_overview","FullyQualifiedName":"localhost/sys/schema_object_overview","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"schema_object_overview","FullyQualifiedName":"localhost/sys/schema_object_overview","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"schema_redundant_indexes","FullyQualifiedName":"localhost/sys/schema_redundant_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"schema_redundant_indexes","FullyQualifiedName":"localhost/sys/schema_redundant_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"schema_redundant_indexes","FullyQualifiedName":"localhost/sys/schema_redundant_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"schema_redundant_indexes","FullyQualifiedName":"localhost/sys/schema_redundant_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"schema_redundant_indexes","FullyQualifiedName":"localhost/sys/schema_redundant_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"schema_redundant_indexes","FullyQualifiedName":"localhost/sys/schema_redundant_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"schema_redundant_indexes","FullyQualifiedName":"localhost/sys/schema_redundant_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"schema_redundant_indexes","FullyQualifiedName":"localhost/sys/schema_redundant_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"schema_redundant_indexes","FullyQualifiedName":"localhost/sys/schema_redundant_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"schema_redundant_indexes","FullyQualifiedName":"localhost/sys/schema_redundant_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"schema_redundant_indexes","FullyQualifiedName":"localhost/sys/schema_redundant_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"schema_redundant_indexes","FullyQualifiedName":"localhost/sys/schema_redundant_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"schema_table_lock_waits","FullyQualifiedName":"localhost/sys/schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"schema_table_lock_waits","FullyQualifiedName":"localhost/sys/schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"schema_table_lock_waits","FullyQualifiedName":"localhost/sys/schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"schema_table_lock_waits","FullyQualifiedName":"localhost/sys/schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"schema_table_lock_waits","FullyQualifiedName":"localhost/sys/schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"schema_table_lock_waits","FullyQualifiedName":"localhost/sys/schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"schema_table_lock_waits","FullyQualifiedName":"localhost/sys/schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"schema_table_lock_waits","FullyQualifiedName":"localhost/sys/schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"schema_table_lock_waits","FullyQualifiedName":"localhost/sys/schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"schema_table_lock_waits","FullyQualifiedName":"localhost/sys/schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"schema_table_lock_waits","FullyQualifiedName":"localhost/sys/schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"schema_table_lock_waits","FullyQualifiedName":"localhost/sys/schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"schema_table_statistics","FullyQualifiedName":"localhost/sys/schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"schema_table_statistics","FullyQualifiedName":"localhost/sys/schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"schema_table_statistics","FullyQualifiedName":"localhost/sys/schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"schema_table_statistics","FullyQualifiedName":"localhost/sys/schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"schema_table_statistics","FullyQualifiedName":"localhost/sys/schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"schema_table_statistics","FullyQualifiedName":"localhost/sys/schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"schema_table_statistics","FullyQualifiedName":"localhost/sys/schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"schema_table_statistics","FullyQualifiedName":"localhost/sys/schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"schema_table_statistics","FullyQualifiedName":"localhost/sys/schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"schema_table_statistics","FullyQualifiedName":"localhost/sys/schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"schema_table_statistics","FullyQualifiedName":"localhost/sys/schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"schema_table_statistics","FullyQualifiedName":"localhost/sys/schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"schema_unused_indexes","FullyQualifiedName":"localhost/sys/schema_unused_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"schema_unused_indexes","FullyQualifiedName":"localhost/sys/schema_unused_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"schema_unused_indexes","FullyQualifiedName":"localhost/sys/schema_unused_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"schema_unused_indexes","FullyQualifiedName":"localhost/sys/schema_unused_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"schema_unused_indexes","FullyQualifiedName":"localhost/sys/schema_unused_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"schema_unused_indexes","FullyQualifiedName":"localhost/sys/schema_unused_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"schema_unused_indexes","FullyQualifiedName":"localhost/sys/schema_unused_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"schema_unused_indexes","FullyQualifiedName":"localhost/sys/schema_unused_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"schema_unused_indexes","FullyQualifiedName":"localhost/sys/schema_unused_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"schema_unused_indexes","FullyQualifiedName":"localhost/sys/schema_unused_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"schema_unused_indexes","FullyQualifiedName":"localhost/sys/schema_unused_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"schema_unused_indexes","FullyQualifiedName":"localhost/sys/schema_unused_indexes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"server_cost","FullyQualifiedName":"localhost/mysql/server_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"server_cost","FullyQualifiedName":"localhost/mysql/server_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"server_cost","FullyQualifiedName":"localhost/mysql/server_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"server_cost","FullyQualifiedName":"localhost/mysql/server_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"server_cost","FullyQualifiedName":"localhost/mysql/server_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"server_cost","FullyQualifiedName":"localhost/mysql/server_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"server_cost","FullyQualifiedName":"localhost/mysql/server_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"server_cost","FullyQualifiedName":"localhost/mysql/server_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"server_cost","FullyQualifiedName":"localhost/mysql/server_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"server_cost","FullyQualifiedName":"localhost/mysql/server_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"server_cost","FullyQualifiedName":"localhost/mysql/server_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"server_cost","FullyQualifiedName":"localhost/mysql/server_cost","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"servers","FullyQualifiedName":"localhost/mysql/servers","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"servers","FullyQualifiedName":"localhost/mysql/servers","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"servers","FullyQualifiedName":"localhost/mysql/servers","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"servers","FullyQualifiedName":"localhost/mysql/servers","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"servers","FullyQualifiedName":"localhost/mysql/servers","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"servers","FullyQualifiedName":"localhost/mysql/servers","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"servers","FullyQualifiedName":"localhost/mysql/servers","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"servers","FullyQualifiedName":"localhost/mysql/servers","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"servers","FullyQualifiedName":"localhost/mysql/servers","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"servers","FullyQualifiedName":"localhost/mysql/servers","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"servers","FullyQualifiedName":"localhost/mysql/servers","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"servers","FullyQualifiedName":"localhost/mysql/servers","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"session","FullyQualifiedName":"localhost/sys/session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"session","FullyQualifiedName":"localhost/sys/session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"session","FullyQualifiedName":"localhost/sys/session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"session","FullyQualifiedName":"localhost/sys/session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"session","FullyQualifiedName":"localhost/sys/session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"session","FullyQualifiedName":"localhost/sys/session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"session","FullyQualifiedName":"localhost/sys/session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"session","FullyQualifiedName":"localhost/sys/session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"session","FullyQualifiedName":"localhost/sys/session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"session","FullyQualifiedName":"localhost/sys/session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"session","FullyQualifiedName":"localhost/sys/session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"session","FullyQualifiedName":"localhost/sys/session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"session_account_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_account_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"session_account_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_account_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"session_account_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_account_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"session_account_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_account_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"session_account_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_account_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"session_account_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_account_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"session_account_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_account_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"session_account_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_account_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"session_account_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_account_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"session_account_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_account_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"session_account_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_account_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"session_account_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_account_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"session_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"session_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"session_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"session_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"session_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"session_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"session_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"session_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"session_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"session_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"session_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"session_connect_attrs","FullyQualifiedName":"localhost/performance_schema/session_connect_attrs","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"session_ssl_status","FullyQualifiedName":"localhost/sys/session_ssl_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"session_ssl_status","FullyQualifiedName":"localhost/sys/session_ssl_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"session_ssl_status","FullyQualifiedName":"localhost/sys/session_ssl_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"session_ssl_status","FullyQualifiedName":"localhost/sys/session_ssl_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"session_ssl_status","FullyQualifiedName":"localhost/sys/session_ssl_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"session_ssl_status","FullyQualifiedName":"localhost/sys/session_ssl_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"session_ssl_status","FullyQualifiedName":"localhost/sys/session_ssl_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"session_ssl_status","FullyQualifiedName":"localhost/sys/session_ssl_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"session_ssl_status","FullyQualifiedName":"localhost/sys/session_ssl_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"session_ssl_status","FullyQualifiedName":"localhost/sys/session_ssl_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"session_ssl_status","FullyQualifiedName":"localhost/sys/session_ssl_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"session_ssl_status","FullyQualifiedName":"localhost/sys/session_ssl_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"session_status","FullyQualifiedName":"localhost/performance_schema/session_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"session_status","FullyQualifiedName":"localhost/performance_schema/session_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"session_status","FullyQualifiedName":"localhost/performance_schema/session_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"session_status","FullyQualifiedName":"localhost/performance_schema/session_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"session_status","FullyQualifiedName":"localhost/performance_schema/session_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"session_status","FullyQualifiedName":"localhost/performance_schema/session_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"session_status","FullyQualifiedName":"localhost/performance_schema/session_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"session_status","FullyQualifiedName":"localhost/performance_schema/session_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"session_status","FullyQualifiedName":"localhost/performance_schema/session_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"session_status","FullyQualifiedName":"localhost/performance_schema/session_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"session_status","FullyQualifiedName":"localhost/performance_schema/session_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"session_status","FullyQualifiedName":"localhost/performance_schema/session_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"session_variables","FullyQualifiedName":"localhost/performance_schema/session_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"session_variables","FullyQualifiedName":"localhost/performance_schema/session_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"session_variables","FullyQualifiedName":"localhost/performance_schema/session_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"session_variables","FullyQualifiedName":"localhost/performance_schema/session_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"session_variables","FullyQualifiedName":"localhost/performance_schema/session_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"session_variables","FullyQualifiedName":"localhost/performance_schema/session_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"session_variables","FullyQualifiedName":"localhost/performance_schema/session_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"session_variables","FullyQualifiedName":"localhost/performance_schema/session_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"session_variables","FullyQualifiedName":"localhost/performance_schema/session_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"session_variables","FullyQualifiedName":"localhost/performance_schema/session_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"session_variables","FullyQualifiedName":"localhost/performance_schema/session_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"session_variables","FullyQualifiedName":"localhost/performance_schema/session_variables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"setup_actors","FullyQualifiedName":"localhost/performance_schema/setup_actors","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"setup_actors","FullyQualifiedName":"localhost/performance_schema/setup_actors","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"setup_actors","FullyQualifiedName":"localhost/performance_schema/setup_actors","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"setup_actors","FullyQualifiedName":"localhost/performance_schema/setup_actors","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"setup_actors","FullyQualifiedName":"localhost/performance_schema/setup_actors","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"setup_actors","FullyQualifiedName":"localhost/performance_schema/setup_actors","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"setup_actors","FullyQualifiedName":"localhost/performance_schema/setup_actors","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"setup_actors","FullyQualifiedName":"localhost/performance_schema/setup_actors","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"setup_actors","FullyQualifiedName":"localhost/performance_schema/setup_actors","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"setup_actors","FullyQualifiedName":"localhost/performance_schema/setup_actors","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"setup_actors","FullyQualifiedName":"localhost/performance_schema/setup_actors","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"setup_actors","FullyQualifiedName":"localhost/performance_schema/setup_actors","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"setup_consumers","FullyQualifiedName":"localhost/performance_schema/setup_consumers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"setup_consumers","FullyQualifiedName":"localhost/performance_schema/setup_consumers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"setup_consumers","FullyQualifiedName":"localhost/performance_schema/setup_consumers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"setup_consumers","FullyQualifiedName":"localhost/performance_schema/setup_consumers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"setup_consumers","FullyQualifiedName":"localhost/performance_schema/setup_consumers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"setup_consumers","FullyQualifiedName":"localhost/performance_schema/setup_consumers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"setup_consumers","FullyQualifiedName":"localhost/performance_schema/setup_consumers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"setup_consumers","FullyQualifiedName":"localhost/performance_schema/setup_consumers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"setup_consumers","FullyQualifiedName":"localhost/performance_schema/setup_consumers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"setup_consumers","FullyQualifiedName":"localhost/performance_schema/setup_consumers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"setup_consumers","FullyQualifiedName":"localhost/performance_schema/setup_consumers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"setup_consumers","FullyQualifiedName":"localhost/performance_schema/setup_consumers","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"setup_instruments","FullyQualifiedName":"localhost/performance_schema/setup_instruments","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"setup_instruments","FullyQualifiedName":"localhost/performance_schema/setup_instruments","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"setup_instruments","FullyQualifiedName":"localhost/performance_schema/setup_instruments","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"setup_instruments","FullyQualifiedName":"localhost/performance_schema/setup_instruments","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"setup_instruments","FullyQualifiedName":"localhost/performance_schema/setup_instruments","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"setup_instruments","FullyQualifiedName":"localhost/performance_schema/setup_instruments","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"setup_instruments","FullyQualifiedName":"localhost/performance_schema/setup_instruments","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"setup_instruments","FullyQualifiedName":"localhost/performance_schema/setup_instruments","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"setup_instruments","FullyQualifiedName":"localhost/performance_schema/setup_instruments","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"setup_instruments","FullyQualifiedName":"localhost/performance_schema/setup_instruments","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"setup_instruments","FullyQualifiedName":"localhost/performance_schema/setup_instruments","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"setup_instruments","FullyQualifiedName":"localhost/performance_schema/setup_instruments","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"setup_meters","FullyQualifiedName":"localhost/performance_schema/setup_meters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"setup_meters","FullyQualifiedName":"localhost/performance_schema/setup_meters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"setup_meters","FullyQualifiedName":"localhost/performance_schema/setup_meters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"setup_meters","FullyQualifiedName":"localhost/performance_schema/setup_meters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"setup_meters","FullyQualifiedName":"localhost/performance_schema/setup_meters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"setup_meters","FullyQualifiedName":"localhost/performance_schema/setup_meters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"setup_meters","FullyQualifiedName":"localhost/performance_schema/setup_meters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"setup_meters","FullyQualifiedName":"localhost/performance_schema/setup_meters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"setup_meters","FullyQualifiedName":"localhost/performance_schema/setup_meters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"setup_meters","FullyQualifiedName":"localhost/performance_schema/setup_meters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"setup_meters","FullyQualifiedName":"localhost/performance_schema/setup_meters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"setup_meters","FullyQualifiedName":"localhost/performance_schema/setup_meters","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"setup_metrics","FullyQualifiedName":"localhost/performance_schema/setup_metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"setup_metrics","FullyQualifiedName":"localhost/performance_schema/setup_metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"setup_metrics","FullyQualifiedName":"localhost/performance_schema/setup_metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"setup_metrics","FullyQualifiedName":"localhost/performance_schema/setup_metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"setup_metrics","FullyQualifiedName":"localhost/performance_schema/setup_metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"setup_metrics","FullyQualifiedName":"localhost/performance_schema/setup_metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"setup_metrics","FullyQualifiedName":"localhost/performance_schema/setup_metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"setup_metrics","FullyQualifiedName":"localhost/performance_schema/setup_metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"setup_metrics","FullyQualifiedName":"localhost/performance_schema/setup_metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"setup_metrics","FullyQualifiedName":"localhost/performance_schema/setup_metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"setup_metrics","FullyQualifiedName":"localhost/performance_schema/setup_metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"setup_metrics","FullyQualifiedName":"localhost/performance_schema/setup_metrics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"setup_objects","FullyQualifiedName":"localhost/performance_schema/setup_objects","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"setup_objects","FullyQualifiedName":"localhost/performance_schema/setup_objects","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"setup_objects","FullyQualifiedName":"localhost/performance_schema/setup_objects","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"setup_objects","FullyQualifiedName":"localhost/performance_schema/setup_objects","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"setup_objects","FullyQualifiedName":"localhost/performance_schema/setup_objects","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"setup_objects","FullyQualifiedName":"localhost/performance_schema/setup_objects","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"setup_objects","FullyQualifiedName":"localhost/performance_schema/setup_objects","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"setup_objects","FullyQualifiedName":"localhost/performance_schema/setup_objects","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"setup_objects","FullyQualifiedName":"localhost/performance_schema/setup_objects","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"setup_objects","FullyQualifiedName":"localhost/performance_schema/setup_objects","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"setup_objects","FullyQualifiedName":"localhost/performance_schema/setup_objects","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"setup_objects","FullyQualifiedName":"localhost/performance_schema/setup_objects","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"setup_threads","FullyQualifiedName":"localhost/performance_schema/setup_threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"setup_threads","FullyQualifiedName":"localhost/performance_schema/setup_threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"setup_threads","FullyQualifiedName":"localhost/performance_schema/setup_threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"setup_threads","FullyQualifiedName":"localhost/performance_schema/setup_threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"setup_threads","FullyQualifiedName":"localhost/performance_schema/setup_threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"setup_threads","FullyQualifiedName":"localhost/performance_schema/setup_threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"setup_threads","FullyQualifiedName":"localhost/performance_schema/setup_threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"setup_threads","FullyQualifiedName":"localhost/performance_schema/setup_threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"setup_threads","FullyQualifiedName":"localhost/performance_schema/setup_threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"setup_threads","FullyQualifiedName":"localhost/performance_schema/setup_threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"setup_threads","FullyQualifiedName":"localhost/performance_schema/setup_threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"setup_threads","FullyQualifiedName":"localhost/performance_schema/setup_threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"slave_master_info","FullyQualifiedName":"localhost/mysql/slave_master_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"slave_master_info","FullyQualifiedName":"localhost/mysql/slave_master_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"slave_master_info","FullyQualifiedName":"localhost/mysql/slave_master_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"slave_master_info","FullyQualifiedName":"localhost/mysql/slave_master_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"slave_master_info","FullyQualifiedName":"localhost/mysql/slave_master_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"slave_master_info","FullyQualifiedName":"localhost/mysql/slave_master_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"slave_master_info","FullyQualifiedName":"localhost/mysql/slave_master_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"slave_master_info","FullyQualifiedName":"localhost/mysql/slave_master_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"slave_master_info","FullyQualifiedName":"localhost/mysql/slave_master_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"slave_master_info","FullyQualifiedName":"localhost/mysql/slave_master_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"slave_master_info","FullyQualifiedName":"localhost/mysql/slave_master_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"slave_master_info","FullyQualifiedName":"localhost/mysql/slave_master_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"slave_relay_log_info","FullyQualifiedName":"localhost/mysql/slave_relay_log_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"slave_relay_log_info","FullyQualifiedName":"localhost/mysql/slave_relay_log_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"slave_relay_log_info","FullyQualifiedName":"localhost/mysql/slave_relay_log_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"slave_relay_log_info","FullyQualifiedName":"localhost/mysql/slave_relay_log_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"slave_relay_log_info","FullyQualifiedName":"localhost/mysql/slave_relay_log_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"slave_relay_log_info","FullyQualifiedName":"localhost/mysql/slave_relay_log_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"slave_relay_log_info","FullyQualifiedName":"localhost/mysql/slave_relay_log_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"slave_relay_log_info","FullyQualifiedName":"localhost/mysql/slave_relay_log_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"slave_relay_log_info","FullyQualifiedName":"localhost/mysql/slave_relay_log_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"slave_relay_log_info","FullyQualifiedName":"localhost/mysql/slave_relay_log_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"slave_relay_log_info","FullyQualifiedName":"localhost/mysql/slave_relay_log_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"slave_relay_log_info","FullyQualifiedName":"localhost/mysql/slave_relay_log_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"slave_worker_info","FullyQualifiedName":"localhost/mysql/slave_worker_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"slave_worker_info","FullyQualifiedName":"localhost/mysql/slave_worker_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"slave_worker_info","FullyQualifiedName":"localhost/mysql/slave_worker_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"slave_worker_info","FullyQualifiedName":"localhost/mysql/slave_worker_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"slave_worker_info","FullyQualifiedName":"localhost/mysql/slave_worker_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"slave_worker_info","FullyQualifiedName":"localhost/mysql/slave_worker_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"slave_worker_info","FullyQualifiedName":"localhost/mysql/slave_worker_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"slave_worker_info","FullyQualifiedName":"localhost/mysql/slave_worker_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"slave_worker_info","FullyQualifiedName":"localhost/mysql/slave_worker_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"slave_worker_info","FullyQualifiedName":"localhost/mysql/slave_worker_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"slave_worker_info","FullyQualifiedName":"localhost/mysql/slave_worker_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"slave_worker_info","FullyQualifiedName":"localhost/mysql/slave_worker_info","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"slow_log","FullyQualifiedName":"localhost/mysql/slow_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"slow_log","FullyQualifiedName":"localhost/mysql/slow_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"slow_log","FullyQualifiedName":"localhost/mysql/slow_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"slow_log","FullyQualifiedName":"localhost/mysql/slow_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"slow_log","FullyQualifiedName":"localhost/mysql/slow_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"slow_log","FullyQualifiedName":"localhost/mysql/slow_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"slow_log","FullyQualifiedName":"localhost/mysql/slow_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"slow_log","FullyQualifiedName":"localhost/mysql/slow_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"slow_log","FullyQualifiedName":"localhost/mysql/slow_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"slow_log","FullyQualifiedName":"localhost/mysql/slow_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"slow_log","FullyQualifiedName":"localhost/mysql/slow_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"slow_log","FullyQualifiedName":"localhost/mysql/slow_log","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"socket_instances","FullyQualifiedName":"localhost/performance_schema/socket_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"socket_instances","FullyQualifiedName":"localhost/performance_schema/socket_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"socket_instances","FullyQualifiedName":"localhost/performance_schema/socket_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"socket_instances","FullyQualifiedName":"localhost/performance_schema/socket_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"socket_instances","FullyQualifiedName":"localhost/performance_schema/socket_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"socket_instances","FullyQualifiedName":"localhost/performance_schema/socket_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"socket_instances","FullyQualifiedName":"localhost/performance_schema/socket_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"socket_instances","FullyQualifiedName":"localhost/performance_schema/socket_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"socket_instances","FullyQualifiedName":"localhost/performance_schema/socket_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"socket_instances","FullyQualifiedName":"localhost/performance_schema/socket_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"socket_instances","FullyQualifiedName":"localhost/performance_schema/socket_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"socket_instances","FullyQualifiedName":"localhost/performance_schema/socket_instances","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"socket_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"socket_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"socket_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"socket_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"socket_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"socket_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"socket_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"socket_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"socket_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"socket_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"socket_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"socket_summary_by_event_name","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_event_name","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"socket_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"socket_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"socket_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"socket_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"socket_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"socket_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"socket_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"socket_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"socket_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"socket_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"socket_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"socket_summary_by_instance","FullyQualifiedName":"localhost/performance_schema/socket_summary_by_instance","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"statement_analysis","FullyQualifiedName":"localhost/sys/statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"statement_analysis","FullyQualifiedName":"localhost/sys/statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"statement_analysis","FullyQualifiedName":"localhost/sys/statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"statement_analysis","FullyQualifiedName":"localhost/sys/statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"statement_analysis","FullyQualifiedName":"localhost/sys/statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"statement_analysis","FullyQualifiedName":"localhost/sys/statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"statement_analysis","FullyQualifiedName":"localhost/sys/statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"statement_analysis","FullyQualifiedName":"localhost/sys/statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"statement_analysis","FullyQualifiedName":"localhost/sys/statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"statement_analysis","FullyQualifiedName":"localhost/sys/statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"statement_analysis","FullyQualifiedName":"localhost/sys/statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"statement_analysis","FullyQualifiedName":"localhost/sys/statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"statement_performance_analyzer","FullyQualifiedName":"localhost/sys/statement_performance_analyzer","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"statement_performance_analyzer","FullyQualifiedName":"localhost/sys/statement_performance_analyzer","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"statements_with_sorting","FullyQualifiedName":"localhost/sys/statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"statements_with_sorting","FullyQualifiedName":"localhost/sys/statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"statements_with_sorting","FullyQualifiedName":"localhost/sys/statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"statements_with_sorting","FullyQualifiedName":"localhost/sys/statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"statements_with_sorting","FullyQualifiedName":"localhost/sys/statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"statements_with_sorting","FullyQualifiedName":"localhost/sys/statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"statements_with_sorting","FullyQualifiedName":"localhost/sys/statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"statements_with_sorting","FullyQualifiedName":"localhost/sys/statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"statements_with_sorting","FullyQualifiedName":"localhost/sys/statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"statements_with_sorting","FullyQualifiedName":"localhost/sys/statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"statements_with_sorting","FullyQualifiedName":"localhost/sys/statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"statements_with_sorting","FullyQualifiedName":"localhost/sys/statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"statements_with_temp_tables","FullyQualifiedName":"localhost/sys/statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"statements_with_temp_tables","FullyQualifiedName":"localhost/sys/statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"statements_with_temp_tables","FullyQualifiedName":"localhost/sys/statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"statements_with_temp_tables","FullyQualifiedName":"localhost/sys/statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"statements_with_temp_tables","FullyQualifiedName":"localhost/sys/statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"statements_with_temp_tables","FullyQualifiedName":"localhost/sys/statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"statements_with_temp_tables","FullyQualifiedName":"localhost/sys/statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"statements_with_temp_tables","FullyQualifiedName":"localhost/sys/statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"statements_with_temp_tables","FullyQualifiedName":"localhost/sys/statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"statements_with_temp_tables","FullyQualifiedName":"localhost/sys/statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"statements_with_temp_tables","FullyQualifiedName":"localhost/sys/statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"statements_with_temp_tables","FullyQualifiedName":"localhost/sys/statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"status_by_account","FullyQualifiedName":"localhost/performance_schema/status_by_account","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"status_by_account","FullyQualifiedName":"localhost/performance_schema/status_by_account","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"status_by_account","FullyQualifiedName":"localhost/performance_schema/status_by_account","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"status_by_account","FullyQualifiedName":"localhost/performance_schema/status_by_account","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"status_by_account","FullyQualifiedName":"localhost/performance_schema/status_by_account","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"status_by_account","FullyQualifiedName":"localhost/performance_schema/status_by_account","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"status_by_account","FullyQualifiedName":"localhost/performance_schema/status_by_account","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"status_by_account","FullyQualifiedName":"localhost/performance_schema/status_by_account","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"status_by_account","FullyQualifiedName":"localhost/performance_schema/status_by_account","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"status_by_account","FullyQualifiedName":"localhost/performance_schema/status_by_account","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"status_by_account","FullyQualifiedName":"localhost/performance_schema/status_by_account","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"status_by_account","FullyQualifiedName":"localhost/performance_schema/status_by_account","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"status_by_host","FullyQualifiedName":"localhost/performance_schema/status_by_host","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"status_by_host","FullyQualifiedName":"localhost/performance_schema/status_by_host","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"status_by_host","FullyQualifiedName":"localhost/performance_schema/status_by_host","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"status_by_host","FullyQualifiedName":"localhost/performance_schema/status_by_host","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"status_by_host","FullyQualifiedName":"localhost/performance_schema/status_by_host","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"status_by_host","FullyQualifiedName":"localhost/performance_schema/status_by_host","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"status_by_host","FullyQualifiedName":"localhost/performance_schema/status_by_host","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"status_by_host","FullyQualifiedName":"localhost/performance_schema/status_by_host","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"status_by_host","FullyQualifiedName":"localhost/performance_schema/status_by_host","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"status_by_host","FullyQualifiedName":"localhost/performance_schema/status_by_host","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"status_by_host","FullyQualifiedName":"localhost/performance_schema/status_by_host","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"status_by_host","FullyQualifiedName":"localhost/performance_schema/status_by_host","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"status_by_thread","FullyQualifiedName":"localhost/performance_schema/status_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"status_by_thread","FullyQualifiedName":"localhost/performance_schema/status_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"status_by_thread","FullyQualifiedName":"localhost/performance_schema/status_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"status_by_thread","FullyQualifiedName":"localhost/performance_schema/status_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"status_by_thread","FullyQualifiedName":"localhost/performance_schema/status_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"status_by_thread","FullyQualifiedName":"localhost/performance_schema/status_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"status_by_thread","FullyQualifiedName":"localhost/performance_schema/status_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"status_by_thread","FullyQualifiedName":"localhost/performance_schema/status_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"status_by_thread","FullyQualifiedName":"localhost/performance_schema/status_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"status_by_thread","FullyQualifiedName":"localhost/performance_schema/status_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"status_by_thread","FullyQualifiedName":"localhost/performance_schema/status_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"status_by_thread","FullyQualifiedName":"localhost/performance_schema/status_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"status_by_user","FullyQualifiedName":"localhost/performance_schema/status_by_user","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"status_by_user","FullyQualifiedName":"localhost/performance_schema/status_by_user","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"status_by_user","FullyQualifiedName":"localhost/performance_schema/status_by_user","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"status_by_user","FullyQualifiedName":"localhost/performance_schema/status_by_user","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"status_by_user","FullyQualifiedName":"localhost/performance_schema/status_by_user","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"status_by_user","FullyQualifiedName":"localhost/performance_schema/status_by_user","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"status_by_user","FullyQualifiedName":"localhost/performance_schema/status_by_user","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"status_by_user","FullyQualifiedName":"localhost/performance_schema/status_by_user","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"status_by_user","FullyQualifiedName":"localhost/performance_schema/status_by_user","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"status_by_user","FullyQualifiedName":"localhost/performance_schema/status_by_user","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"status_by_user","FullyQualifiedName":"localhost/performance_schema/status_by_user","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"status_by_user","FullyQualifiedName":"localhost/performance_schema/status_by_user","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"CREATE ROUTINE","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"CREATE TEMPORARY TABLES","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"EVENT","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"LOCK TABLES","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"sys_config","FullyQualifiedName":"localhost/sys/sys_config","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"sys_config","FullyQualifiedName":"localhost/sys/sys_config","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"sys_config","FullyQualifiedName":"localhost/sys/sys_config","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"sys_config","FullyQualifiedName":"localhost/sys/sys_config","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"sys_config","FullyQualifiedName":"localhost/sys/sys_config","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"sys_config","FullyQualifiedName":"localhost/sys/sys_config","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"sys_config","FullyQualifiedName":"localhost/sys/sys_config","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"sys_config","FullyQualifiedName":"localhost/sys/sys_config","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"sys_config","FullyQualifiedName":"localhost/sys/sys_config","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"sys_config","FullyQualifiedName":"localhost/sys/sys_config","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"sys_config","FullyQualifiedName":"localhost/sys/sys_config","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"sys_config","FullyQualifiedName":"localhost/sys/sys_config","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"sys_get_config","FullyQualifiedName":"localhost/sys/sys_get_config","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"sys_get_config","FullyQualifiedName":"localhost/sys/sys_get_config","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"table_exists","FullyQualifiedName":"localhost/sys/table_exists","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"table_exists","FullyQualifiedName":"localhost/sys/table_exists","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"table_handles","FullyQualifiedName":"localhost/performance_schema/table_handles","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"table_handles","FullyQualifiedName":"localhost/performance_schema/table_handles","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"table_handles","FullyQualifiedName":"localhost/performance_schema/table_handles","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"table_handles","FullyQualifiedName":"localhost/performance_schema/table_handles","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"table_handles","FullyQualifiedName":"localhost/performance_schema/table_handles","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"table_handles","FullyQualifiedName":"localhost/performance_schema/table_handles","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"table_handles","FullyQualifiedName":"localhost/performance_schema/table_handles","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"table_handles","FullyQualifiedName":"localhost/performance_schema/table_handles","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"table_handles","FullyQualifiedName":"localhost/performance_schema/table_handles","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"table_handles","FullyQualifiedName":"localhost/performance_schema/table_handles","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"table_handles","FullyQualifiedName":"localhost/performance_schema/table_handles","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"table_handles","FullyQualifiedName":"localhost/performance_schema/table_handles","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_index_usage","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_index_usage","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_index_usage","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_index_usage","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_index_usage","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_index_usage","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_index_usage","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_index_usage","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_index_usage","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_index_usage","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_index_usage","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_index_usage","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_index_usage","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_index_usage","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_index_usage","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_index_usage","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_index_usage","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_index_usage","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_index_usage","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_index_usage","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_index_usage","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_index_usage","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_index_usage","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_index_usage","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"table_io_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_io_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"table_lock_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_lock_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"table_lock_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_lock_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"table_lock_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_lock_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"table_lock_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_lock_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"table_lock_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_lock_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"table_lock_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_lock_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"table_lock_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_lock_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"table_lock_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_lock_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"table_lock_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_lock_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"table_lock_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_lock_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"table_lock_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_lock_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"table_lock_waits_summary_by_table","FullyQualifiedName":"localhost/performance_schema/table_lock_waits_summary_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"tables_priv","FullyQualifiedName":"localhost/mysql/tables_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"tables_priv","FullyQualifiedName":"localhost/mysql/tables_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"tables_priv","FullyQualifiedName":"localhost/mysql/tables_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"tables_priv","FullyQualifiedName":"localhost/mysql/tables_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"tables_priv","FullyQualifiedName":"localhost/mysql/tables_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"tables_priv","FullyQualifiedName":"localhost/mysql/tables_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"tables_priv","FullyQualifiedName":"localhost/mysql/tables_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"tables_priv","FullyQualifiedName":"localhost/mysql/tables_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"tables_priv","FullyQualifiedName":"localhost/mysql/tables_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"tables_priv","FullyQualifiedName":"localhost/mysql/tables_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"tables_priv","FullyQualifiedName":"localhost/mysql/tables_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"tables_priv","FullyQualifiedName":"localhost/mysql/tables_priv","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"threads","FullyQualifiedName":"localhost/performance_schema/threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"threads","FullyQualifiedName":"localhost/performance_schema/threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"threads","FullyQualifiedName":"localhost/performance_schema/threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"threads","FullyQualifiedName":"localhost/performance_schema/threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"threads","FullyQualifiedName":"localhost/performance_schema/threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"threads","FullyQualifiedName":"localhost/performance_schema/threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"threads","FullyQualifiedName":"localhost/performance_schema/threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"threads","FullyQualifiedName":"localhost/performance_schema/threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"threads","FullyQualifiedName":"localhost/performance_schema/threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"threads","FullyQualifiedName":"localhost/performance_schema/threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"threads","FullyQualifiedName":"localhost/performance_schema/threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"threads","FullyQualifiedName":"localhost/performance_schema/threads","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"time_zone","FullyQualifiedName":"localhost/mysql/time_zone","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"time_zone","FullyQualifiedName":"localhost/mysql/time_zone","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"time_zone","FullyQualifiedName":"localhost/mysql/time_zone","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"time_zone","FullyQualifiedName":"localhost/mysql/time_zone","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"time_zone","FullyQualifiedName":"localhost/mysql/time_zone","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"time_zone","FullyQualifiedName":"localhost/mysql/time_zone","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"time_zone","FullyQualifiedName":"localhost/mysql/time_zone","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"time_zone","FullyQualifiedName":"localhost/mysql/time_zone","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"time_zone","FullyQualifiedName":"localhost/mysql/time_zone","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"time_zone","FullyQualifiedName":"localhost/mysql/time_zone","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"time_zone","FullyQualifiedName":"localhost/mysql/time_zone","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"time_zone","FullyQualifiedName":"localhost/mysql/time_zone","Type":"table","Metadata":{"bytes":81920,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"time_zone_leap_second","FullyQualifiedName":"localhost/mysql/time_zone_leap_second","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"time_zone_leap_second","FullyQualifiedName":"localhost/mysql/time_zone_leap_second","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"time_zone_leap_second","FullyQualifiedName":"localhost/mysql/time_zone_leap_second","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"time_zone_leap_second","FullyQualifiedName":"localhost/mysql/time_zone_leap_second","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"time_zone_leap_second","FullyQualifiedName":"localhost/mysql/time_zone_leap_second","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"time_zone_leap_second","FullyQualifiedName":"localhost/mysql/time_zone_leap_second","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"time_zone_leap_second","FullyQualifiedName":"localhost/mysql/time_zone_leap_second","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"time_zone_leap_second","FullyQualifiedName":"localhost/mysql/time_zone_leap_second","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"time_zone_leap_second","FullyQualifiedName":"localhost/mysql/time_zone_leap_second","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"time_zone_leap_second","FullyQualifiedName":"localhost/mysql/time_zone_leap_second","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"time_zone_leap_second","FullyQualifiedName":"localhost/mysql/time_zone_leap_second","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"time_zone_leap_second","FullyQualifiedName":"localhost/mysql/time_zone_leap_second","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"time_zone_name","FullyQualifiedName":"localhost/mysql/time_zone_name","Type":"table","Metadata":{"bytes":245760,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"time_zone_name","FullyQualifiedName":"localhost/mysql/time_zone_name","Type":"table","Metadata":{"bytes":245760,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"time_zone_name","FullyQualifiedName":"localhost/mysql/time_zone_name","Type":"table","Metadata":{"bytes":245760,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"time_zone_name","FullyQualifiedName":"localhost/mysql/time_zone_name","Type":"table","Metadata":{"bytes":245760,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"time_zone_name","FullyQualifiedName":"localhost/mysql/time_zone_name","Type":"table","Metadata":{"bytes":245760,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"time_zone_name","FullyQualifiedName":"localhost/mysql/time_zone_name","Type":"table","Metadata":{"bytes":245760,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"time_zone_name","FullyQualifiedName":"localhost/mysql/time_zone_name","Type":"table","Metadata":{"bytes":245760,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"time_zone_name","FullyQualifiedName":"localhost/mysql/time_zone_name","Type":"table","Metadata":{"bytes":245760,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"time_zone_name","FullyQualifiedName":"localhost/mysql/time_zone_name","Type":"table","Metadata":{"bytes":245760,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"time_zone_name","FullyQualifiedName":"localhost/mysql/time_zone_name","Type":"table","Metadata":{"bytes":245760,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"time_zone_name","FullyQualifiedName":"localhost/mysql/time_zone_name","Type":"table","Metadata":{"bytes":245760,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"time_zone_name","FullyQualifiedName":"localhost/mysql/time_zone_name","Type":"table","Metadata":{"bytes":245760,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"time_zone_transition","FullyQualifiedName":"localhost/mysql/time_zone_transition","Type":"table","Metadata":{"bytes":4734976,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"time_zone_transition","FullyQualifiedName":"localhost/mysql/time_zone_transition","Type":"table","Metadata":{"bytes":4734976,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"time_zone_transition","FullyQualifiedName":"localhost/mysql/time_zone_transition","Type":"table","Metadata":{"bytes":4734976,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"time_zone_transition","FullyQualifiedName":"localhost/mysql/time_zone_transition","Type":"table","Metadata":{"bytes":4734976,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"time_zone_transition","FullyQualifiedName":"localhost/mysql/time_zone_transition","Type":"table","Metadata":{"bytes":4734976,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"time_zone_transition","FullyQualifiedName":"localhost/mysql/time_zone_transition","Type":"table","Metadata":{"bytes":4734976,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"time_zone_transition","FullyQualifiedName":"localhost/mysql/time_zone_transition","Type":"table","Metadata":{"bytes":4734976,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"time_zone_transition","FullyQualifiedName":"localhost/mysql/time_zone_transition","Type":"table","Metadata":{"bytes":4734976,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"time_zone_transition","FullyQualifiedName":"localhost/mysql/time_zone_transition","Type":"table","Metadata":{"bytes":4734976,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"time_zone_transition","FullyQualifiedName":"localhost/mysql/time_zone_transition","Type":"table","Metadata":{"bytes":4734976,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"time_zone_transition","FullyQualifiedName":"localhost/mysql/time_zone_transition","Type":"table","Metadata":{"bytes":4734976,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"time_zone_transition","FullyQualifiedName":"localhost/mysql/time_zone_transition","Type":"table","Metadata":{"bytes":4734976,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"time_zone_transition_type","FullyQualifiedName":"localhost/mysql/time_zone_transition_type","Type":"table","Metadata":{"bytes":475136,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"time_zone_transition_type","FullyQualifiedName":"localhost/mysql/time_zone_transition_type","Type":"table","Metadata":{"bytes":475136,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"time_zone_transition_type","FullyQualifiedName":"localhost/mysql/time_zone_transition_type","Type":"table","Metadata":{"bytes":475136,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"time_zone_transition_type","FullyQualifiedName":"localhost/mysql/time_zone_transition_type","Type":"table","Metadata":{"bytes":475136,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"time_zone_transition_type","FullyQualifiedName":"localhost/mysql/time_zone_transition_type","Type":"table","Metadata":{"bytes":475136,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"time_zone_transition_type","FullyQualifiedName":"localhost/mysql/time_zone_transition_type","Type":"table","Metadata":{"bytes":475136,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"time_zone_transition_type","FullyQualifiedName":"localhost/mysql/time_zone_transition_type","Type":"table","Metadata":{"bytes":475136,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"time_zone_transition_type","FullyQualifiedName":"localhost/mysql/time_zone_transition_type","Type":"table","Metadata":{"bytes":475136,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"time_zone_transition_type","FullyQualifiedName":"localhost/mysql/time_zone_transition_type","Type":"table","Metadata":{"bytes":475136,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"time_zone_transition_type","FullyQualifiedName":"localhost/mysql/time_zone_transition_type","Type":"table","Metadata":{"bytes":475136,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"time_zone_transition_type","FullyQualifiedName":"localhost/mysql/time_zone_transition_type","Type":"table","Metadata":{"bytes":475136,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"time_zone_transition_type","FullyQualifiedName":"localhost/mysql/time_zone_transition_type","Type":"table","Metadata":{"bytes":475136,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"tls_channel_status","FullyQualifiedName":"localhost/performance_schema/tls_channel_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"tls_channel_status","FullyQualifiedName":"localhost/performance_schema/tls_channel_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"tls_channel_status","FullyQualifiedName":"localhost/performance_schema/tls_channel_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"tls_channel_status","FullyQualifiedName":"localhost/performance_schema/tls_channel_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"tls_channel_status","FullyQualifiedName":"localhost/performance_schema/tls_channel_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"tls_channel_status","FullyQualifiedName":"localhost/performance_schema/tls_channel_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"tls_channel_status","FullyQualifiedName":"localhost/performance_schema/tls_channel_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"tls_channel_status","FullyQualifiedName":"localhost/performance_schema/tls_channel_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"tls_channel_status","FullyQualifiedName":"localhost/performance_schema/tls_channel_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"tls_channel_status","FullyQualifiedName":"localhost/performance_schema/tls_channel_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"tls_channel_status","FullyQualifiedName":"localhost/performance_schema/tls_channel_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"tls_channel_status","FullyQualifiedName":"localhost/performance_schema/tls_channel_status","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"user","FullyQualifiedName":"localhost/mysql/user","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"user","FullyQualifiedName":"localhost/mysql/user","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"user","FullyQualifiedName":"localhost/mysql/user","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"user","FullyQualifiedName":"localhost/mysql/user","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"user","FullyQualifiedName":"localhost/mysql/user","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"user","FullyQualifiedName":"localhost/mysql/user","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"user","FullyQualifiedName":"localhost/mysql/user","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"user","FullyQualifiedName":"localhost/mysql/user","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"user","FullyQualifiedName":"localhost/mysql/user","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"user","FullyQualifiedName":"localhost/mysql/user","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"user","FullyQualifiedName":"localhost/mysql/user","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"user","FullyQualifiedName":"localhost/mysql/user","Type":"table","Metadata":{"bytes":16384,"non_existent":false},"Parent":{"Name":"mysql","FullyQualifiedName":"localhost/mysql","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"user_defined_functions","FullyQualifiedName":"localhost/performance_schema/user_defined_functions","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"user_defined_functions","FullyQualifiedName":"localhost/performance_schema/user_defined_functions","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"user_defined_functions","FullyQualifiedName":"localhost/performance_schema/user_defined_functions","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"user_defined_functions","FullyQualifiedName":"localhost/performance_schema/user_defined_functions","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"user_defined_functions","FullyQualifiedName":"localhost/performance_schema/user_defined_functions","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"user_defined_functions","FullyQualifiedName":"localhost/performance_schema/user_defined_functions","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"user_defined_functions","FullyQualifiedName":"localhost/performance_schema/user_defined_functions","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"user_defined_functions","FullyQualifiedName":"localhost/performance_schema/user_defined_functions","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"user_defined_functions","FullyQualifiedName":"localhost/performance_schema/user_defined_functions","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"user_defined_functions","FullyQualifiedName":"localhost/performance_schema/user_defined_functions","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"user_defined_functions","FullyQualifiedName":"localhost/performance_schema/user_defined_functions","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"user_defined_functions","FullyQualifiedName":"localhost/performance_schema/user_defined_functions","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"user_summary","FullyQualifiedName":"localhost/sys/user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"user_summary","FullyQualifiedName":"localhost/sys/user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"user_summary","FullyQualifiedName":"localhost/sys/user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"user_summary","FullyQualifiedName":"localhost/sys/user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"user_summary","FullyQualifiedName":"localhost/sys/user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"user_summary","FullyQualifiedName":"localhost/sys/user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"user_summary","FullyQualifiedName":"localhost/sys/user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"user_summary","FullyQualifiedName":"localhost/sys/user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"user_summary","FullyQualifiedName":"localhost/sys/user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"user_summary","FullyQualifiedName":"localhost/sys/user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"user_summary","FullyQualifiedName":"localhost/sys/user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"user_summary","FullyQualifiedName":"localhost/sys/user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io","FullyQualifiedName":"localhost/sys/user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io","FullyQualifiedName":"localhost/sys/user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io","FullyQualifiedName":"localhost/sys/user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io","FullyQualifiedName":"localhost/sys/user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io","FullyQualifiedName":"localhost/sys/user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io","FullyQualifiedName":"localhost/sys/user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io","FullyQualifiedName":"localhost/sys/user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io","FullyQualifiedName":"localhost/sys/user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io","FullyQualifiedName":"localhost/sys/user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io","FullyQualifiedName":"localhost/sys/user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io","FullyQualifiedName":"localhost/sys/user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io","FullyQualifiedName":"localhost/sys/user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"user_summary_by_stages","FullyQualifiedName":"localhost/sys/user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"user_summary_by_stages","FullyQualifiedName":"localhost/sys/user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"user_summary_by_stages","FullyQualifiedName":"localhost/sys/user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"user_summary_by_stages","FullyQualifiedName":"localhost/sys/user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"user_summary_by_stages","FullyQualifiedName":"localhost/sys/user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"user_summary_by_stages","FullyQualifiedName":"localhost/sys/user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"user_summary_by_stages","FullyQualifiedName":"localhost/sys/user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"user_summary_by_stages","FullyQualifiedName":"localhost/sys/user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"user_summary_by_stages","FullyQualifiedName":"localhost/sys/user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"user_summary_by_stages","FullyQualifiedName":"localhost/sys/user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"user_summary_by_stages","FullyQualifiedName":"localhost/sys/user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"user_summary_by_stages","FullyQualifiedName":"localhost/sys/user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"user_variables_by_thread","FullyQualifiedName":"localhost/performance_schema/user_variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"user_variables_by_thread","FullyQualifiedName":"localhost/performance_schema/user_variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"user_variables_by_thread","FullyQualifiedName":"localhost/performance_schema/user_variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"user_variables_by_thread","FullyQualifiedName":"localhost/performance_schema/user_variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"user_variables_by_thread","FullyQualifiedName":"localhost/performance_schema/user_variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"user_variables_by_thread","FullyQualifiedName":"localhost/performance_schema/user_variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"user_variables_by_thread","FullyQualifiedName":"localhost/performance_schema/user_variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"user_variables_by_thread","FullyQualifiedName":"localhost/performance_schema/user_variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"user_variables_by_thread","FullyQualifiedName":"localhost/performance_schema/user_variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"user_variables_by_thread","FullyQualifiedName":"localhost/performance_schema/user_variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"user_variables_by_thread","FullyQualifiedName":"localhost/performance_schema/user_variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"user_variables_by_thread","FullyQualifiedName":"localhost/performance_schema/user_variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"users","FullyQualifiedName":"localhost/performance_schema/users","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"users","FullyQualifiedName":"localhost/performance_schema/users","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"users","FullyQualifiedName":"localhost/performance_schema/users","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"users","FullyQualifiedName":"localhost/performance_schema/users","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"users","FullyQualifiedName":"localhost/performance_schema/users","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"users","FullyQualifiedName":"localhost/performance_schema/users","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"users","FullyQualifiedName":"localhost/performance_schema/users","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"users","FullyQualifiedName":"localhost/performance_schema/users","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"users","FullyQualifiedName":"localhost/performance_schema/users","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"users","FullyQualifiedName":"localhost/performance_schema/users","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"users","FullyQualifiedName":"localhost/performance_schema/users","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"users","FullyQualifiedName":"localhost/performance_schema/users","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"variables_by_thread","FullyQualifiedName":"localhost/performance_schema/variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"variables_by_thread","FullyQualifiedName":"localhost/performance_schema/variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"variables_by_thread","FullyQualifiedName":"localhost/performance_schema/variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"variables_by_thread","FullyQualifiedName":"localhost/performance_schema/variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"variables_by_thread","FullyQualifiedName":"localhost/performance_schema/variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"variables_by_thread","FullyQualifiedName":"localhost/performance_schema/variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"variables_by_thread","FullyQualifiedName":"localhost/performance_schema/variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"variables_by_thread","FullyQualifiedName":"localhost/performance_schema/variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"variables_by_thread","FullyQualifiedName":"localhost/performance_schema/variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"variables_by_thread","FullyQualifiedName":"localhost/performance_schema/variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"variables_by_thread","FullyQualifiedName":"localhost/performance_schema/variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"variables_by_thread","FullyQualifiedName":"localhost/performance_schema/variables_by_thread","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"variables_info","FullyQualifiedName":"localhost/performance_schema/variables_info","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"variables_info","FullyQualifiedName":"localhost/performance_schema/variables_info","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"variables_info","FullyQualifiedName":"localhost/performance_schema/variables_info","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"variables_info","FullyQualifiedName":"localhost/performance_schema/variables_info","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"variables_info","FullyQualifiedName":"localhost/performance_schema/variables_info","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"variables_info","FullyQualifiedName":"localhost/performance_schema/variables_info","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"variables_info","FullyQualifiedName":"localhost/performance_schema/variables_info","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"variables_info","FullyQualifiedName":"localhost/performance_schema/variables_info","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"variables_info","FullyQualifiedName":"localhost/performance_schema/variables_info","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"variables_info","FullyQualifiedName":"localhost/performance_schema/variables_info","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"variables_info","FullyQualifiedName":"localhost/performance_schema/variables_info","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"variables_info","FullyQualifiedName":"localhost/performance_schema/variables_info","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"variables_metadata","FullyQualifiedName":"localhost/performance_schema/variables_metadata","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"variables_metadata","FullyQualifiedName":"localhost/performance_schema/variables_metadata","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"variables_metadata","FullyQualifiedName":"localhost/performance_schema/variables_metadata","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"variables_metadata","FullyQualifiedName":"localhost/performance_schema/variables_metadata","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"variables_metadata","FullyQualifiedName":"localhost/performance_schema/variables_metadata","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"variables_metadata","FullyQualifiedName":"localhost/performance_schema/variables_metadata","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"variables_metadata","FullyQualifiedName":"localhost/performance_schema/variables_metadata","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"variables_metadata","FullyQualifiedName":"localhost/performance_schema/variables_metadata","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"variables_metadata","FullyQualifiedName":"localhost/performance_schema/variables_metadata","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"variables_metadata","FullyQualifiedName":"localhost/performance_schema/variables_metadata","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"variables_metadata","FullyQualifiedName":"localhost/performance_schema/variables_metadata","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"variables_metadata","FullyQualifiedName":"localhost/performance_schema/variables_metadata","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"performance_schema","FullyQualifiedName":"localhost/performance_schema","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"version","FullyQualifiedName":"localhost/sys/version","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"version","FullyQualifiedName":"localhost/sys/version","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"version","FullyQualifiedName":"localhost/sys/version","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"version","FullyQualifiedName":"localhost/sys/version","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"version","FullyQualifiedName":"localhost/sys/version","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"version","FullyQualifiedName":"localhost/sys/version","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"version","FullyQualifiedName":"localhost/sys/version","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"version","FullyQualifiedName":"localhost/sys/version","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"version","FullyQualifiedName":"localhost/sys/version","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"version","FullyQualifiedName":"localhost/sys/version","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"version","FullyQualifiedName":"localhost/sys/version","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"version","FullyQualifiedName":"localhost/sys/version","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"version_major","FullyQualifiedName":"localhost/sys/version_major","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"version_major","FullyQualifiedName":"localhost/sys/version_major","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"version_minor","FullyQualifiedName":"localhost/sys/version_minor","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"version_minor","FullyQualifiedName":"localhost/sys/version_minor","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"version_patch","FullyQualifiedName":"localhost/sys/version_patch","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER ROUTINE","Parent":null}},{"Resource":{"Name":"version_patch","FullyQualifiedName":"localhost/sys/version_patch","Type":"routine","Metadata":{"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"EXECUTE","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"waits_global_by_latency","FullyQualifiedName":"localhost/sys/waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"waits_global_by_latency","FullyQualifiedName":"localhost/sys/waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"waits_global_by_latency","FullyQualifiedName":"localhost/sys/waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"waits_global_by_latency","FullyQualifiedName":"localhost/sys/waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"waits_global_by_latency","FullyQualifiedName":"localhost/sys/waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"waits_global_by_latency","FullyQualifiedName":"localhost/sys/waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"waits_global_by_latency","FullyQualifiedName":"localhost/sys/waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"waits_global_by_latency","FullyQualifiedName":"localhost/sys/waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"waits_global_by_latency","FullyQualifiedName":"localhost/sys/waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"waits_global_by_latency","FullyQualifiedName":"localhost/sys/waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"waits_global_by_latency","FullyQualifiedName":"localhost/sys/waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"waits_global_by_latency","FullyQualifiedName":"localhost/sys/waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$host_summary","FullyQualifiedName":"localhost/sys/x$host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$host_summary","FullyQualifiedName":"localhost/sys/x$host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$host_summary","FullyQualifiedName":"localhost/sys/x$host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$host_summary","FullyQualifiedName":"localhost/sys/x$host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$host_summary","FullyQualifiedName":"localhost/sys/x$host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$host_summary","FullyQualifiedName":"localhost/sys/x$host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$host_summary","FullyQualifiedName":"localhost/sys/x$host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$host_summary","FullyQualifiedName":"localhost/sys/x$host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$host_summary","FullyQualifiedName":"localhost/sys/x$host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$host_summary","FullyQualifiedName":"localhost/sys/x$host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$host_summary","FullyQualifiedName":"localhost/sys/x$host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$host_summary","FullyQualifiedName":"localhost/sys/x$host_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$host_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$host_summary_by_stages","FullyQualifiedName":"localhost/sys/x$host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$host_summary_by_stages","FullyQualifiedName":"localhost/sys/x$host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$host_summary_by_stages","FullyQualifiedName":"localhost/sys/x$host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$host_summary_by_stages","FullyQualifiedName":"localhost/sys/x$host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$host_summary_by_stages","FullyQualifiedName":"localhost/sys/x$host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$host_summary_by_stages","FullyQualifiedName":"localhost/sys/x$host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$host_summary_by_stages","FullyQualifiedName":"localhost/sys/x$host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$host_summary_by_stages","FullyQualifiedName":"localhost/sys/x$host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$host_summary_by_stages","FullyQualifiedName":"localhost/sys/x$host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$host_summary_by_stages","FullyQualifiedName":"localhost/sys/x$host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$host_summary_by_stages","FullyQualifiedName":"localhost/sys/x$host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$host_summary_by_stages","FullyQualifiedName":"localhost/sys/x$host_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$host_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$host_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_schema","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_schema","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$innodb_buffer_stats_by_table","FullyQualifiedName":"localhost/sys/x$innodb_buffer_stats_by_table","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$innodb_lock_waits","FullyQualifiedName":"localhost/sys/x$innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$innodb_lock_waits","FullyQualifiedName":"localhost/sys/x$innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$innodb_lock_waits","FullyQualifiedName":"localhost/sys/x$innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$innodb_lock_waits","FullyQualifiedName":"localhost/sys/x$innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$innodb_lock_waits","FullyQualifiedName":"localhost/sys/x$innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$innodb_lock_waits","FullyQualifiedName":"localhost/sys/x$innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$innodb_lock_waits","FullyQualifiedName":"localhost/sys/x$innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$innodb_lock_waits","FullyQualifiedName":"localhost/sys/x$innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$innodb_lock_waits","FullyQualifiedName":"localhost/sys/x$innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$innodb_lock_waits","FullyQualifiedName":"localhost/sys/x$innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$innodb_lock_waits","FullyQualifiedName":"localhost/sys/x$innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$innodb_lock_waits","FullyQualifiedName":"localhost/sys/x$innodb_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/x$io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/x$io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/x$io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/x$io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/x$io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/x$io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/x$io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/x$io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/x$io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/x$io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/x$io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$io_by_thread_by_latency","FullyQualifiedName":"localhost/sys/x$io_by_thread_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$io_global_by_file_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_file_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_bytes","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$io_global_by_wait_by_latency","FullyQualifiedName":"localhost/sys/x$io_global_by_wait_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$latest_file_io","FullyQualifiedName":"localhost/sys/x$latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$latest_file_io","FullyQualifiedName":"localhost/sys/x$latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$latest_file_io","FullyQualifiedName":"localhost/sys/x$latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$latest_file_io","FullyQualifiedName":"localhost/sys/x$latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$latest_file_io","FullyQualifiedName":"localhost/sys/x$latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$latest_file_io","FullyQualifiedName":"localhost/sys/x$latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$latest_file_io","FullyQualifiedName":"localhost/sys/x$latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$latest_file_io","FullyQualifiedName":"localhost/sys/x$latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$latest_file_io","FullyQualifiedName":"localhost/sys/x$latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$latest_file_io","FullyQualifiedName":"localhost/sys/x$latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$latest_file_io","FullyQualifiedName":"localhost/sys/x$latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$latest_file_io","FullyQualifiedName":"localhost/sys/x$latest_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$memory_by_host_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_host_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$memory_by_thread_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_thread_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$memory_by_user_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_by_user_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$memory_global_by_current_bytes","FullyQualifiedName":"localhost/sys/x$memory_global_by_current_bytes","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$memory_global_total","FullyQualifiedName":"localhost/sys/x$memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$memory_global_total","FullyQualifiedName":"localhost/sys/x$memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$memory_global_total","FullyQualifiedName":"localhost/sys/x$memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$memory_global_total","FullyQualifiedName":"localhost/sys/x$memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$memory_global_total","FullyQualifiedName":"localhost/sys/x$memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$memory_global_total","FullyQualifiedName":"localhost/sys/x$memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$memory_global_total","FullyQualifiedName":"localhost/sys/x$memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$memory_global_total","FullyQualifiedName":"localhost/sys/x$memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$memory_global_total","FullyQualifiedName":"localhost/sys/x$memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$memory_global_total","FullyQualifiedName":"localhost/sys/x$memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$memory_global_total","FullyQualifiedName":"localhost/sys/x$memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$memory_global_total","FullyQualifiedName":"localhost/sys/x$memory_global_total","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$processlist","FullyQualifiedName":"localhost/sys/x$processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$processlist","FullyQualifiedName":"localhost/sys/x$processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$processlist","FullyQualifiedName":"localhost/sys/x$processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$processlist","FullyQualifiedName":"localhost/sys/x$processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$processlist","FullyQualifiedName":"localhost/sys/x$processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$processlist","FullyQualifiedName":"localhost/sys/x$processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$processlist","FullyQualifiedName":"localhost/sys/x$processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$processlist","FullyQualifiedName":"localhost/sys/x$processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$processlist","FullyQualifiedName":"localhost/sys/x$processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$processlist","FullyQualifiedName":"localhost/sys/x$processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$processlist","FullyQualifiedName":"localhost/sys/x$processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$processlist","FullyQualifiedName":"localhost/sys/x$processlist","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$ps_digest_95th_percentile_by_avg_us","FullyQualifiedName":"localhost/sys/x$ps_digest_95th_percentile_by_avg_us","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$ps_digest_95th_percentile_by_avg_us","FullyQualifiedName":"localhost/sys/x$ps_digest_95th_percentile_by_avg_us","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$ps_digest_95th_percentile_by_avg_us","FullyQualifiedName":"localhost/sys/x$ps_digest_95th_percentile_by_avg_us","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$ps_digest_95th_percentile_by_avg_us","FullyQualifiedName":"localhost/sys/x$ps_digest_95th_percentile_by_avg_us","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$ps_digest_95th_percentile_by_avg_us","FullyQualifiedName":"localhost/sys/x$ps_digest_95th_percentile_by_avg_us","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$ps_digest_95th_percentile_by_avg_us","FullyQualifiedName":"localhost/sys/x$ps_digest_95th_percentile_by_avg_us","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$ps_digest_95th_percentile_by_avg_us","FullyQualifiedName":"localhost/sys/x$ps_digest_95th_percentile_by_avg_us","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$ps_digest_95th_percentile_by_avg_us","FullyQualifiedName":"localhost/sys/x$ps_digest_95th_percentile_by_avg_us","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$ps_digest_95th_percentile_by_avg_us","FullyQualifiedName":"localhost/sys/x$ps_digest_95th_percentile_by_avg_us","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$ps_digest_95th_percentile_by_avg_us","FullyQualifiedName":"localhost/sys/x$ps_digest_95th_percentile_by_avg_us","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$ps_digest_95th_percentile_by_avg_us","FullyQualifiedName":"localhost/sys/x$ps_digest_95th_percentile_by_avg_us","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$ps_digest_95th_percentile_by_avg_us","FullyQualifiedName":"localhost/sys/x$ps_digest_95th_percentile_by_avg_us","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$ps_digest_avg_latency_distribution","FullyQualifiedName":"localhost/sys/x$ps_digest_avg_latency_distribution","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$ps_digest_avg_latency_distribution","FullyQualifiedName":"localhost/sys/x$ps_digest_avg_latency_distribution","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$ps_digest_avg_latency_distribution","FullyQualifiedName":"localhost/sys/x$ps_digest_avg_latency_distribution","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$ps_digest_avg_latency_distribution","FullyQualifiedName":"localhost/sys/x$ps_digest_avg_latency_distribution","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$ps_digest_avg_latency_distribution","FullyQualifiedName":"localhost/sys/x$ps_digest_avg_latency_distribution","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$ps_digest_avg_latency_distribution","FullyQualifiedName":"localhost/sys/x$ps_digest_avg_latency_distribution","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$ps_digest_avg_latency_distribution","FullyQualifiedName":"localhost/sys/x$ps_digest_avg_latency_distribution","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$ps_digest_avg_latency_distribution","FullyQualifiedName":"localhost/sys/x$ps_digest_avg_latency_distribution","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$ps_digest_avg_latency_distribution","FullyQualifiedName":"localhost/sys/x$ps_digest_avg_latency_distribution","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$ps_digest_avg_latency_distribution","FullyQualifiedName":"localhost/sys/x$ps_digest_avg_latency_distribution","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$ps_digest_avg_latency_distribution","FullyQualifiedName":"localhost/sys/x$ps_digest_avg_latency_distribution","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$ps_digest_avg_latency_distribution","FullyQualifiedName":"localhost/sys/x$ps_digest_avg_latency_distribution","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$ps_schema_table_statistics_io","FullyQualifiedName":"localhost/sys/x$ps_schema_table_statistics_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$ps_schema_table_statistics_io","FullyQualifiedName":"localhost/sys/x$ps_schema_table_statistics_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$ps_schema_table_statistics_io","FullyQualifiedName":"localhost/sys/x$ps_schema_table_statistics_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$ps_schema_table_statistics_io","FullyQualifiedName":"localhost/sys/x$ps_schema_table_statistics_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$ps_schema_table_statistics_io","FullyQualifiedName":"localhost/sys/x$ps_schema_table_statistics_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$ps_schema_table_statistics_io","FullyQualifiedName":"localhost/sys/x$ps_schema_table_statistics_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$ps_schema_table_statistics_io","FullyQualifiedName":"localhost/sys/x$ps_schema_table_statistics_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$ps_schema_table_statistics_io","FullyQualifiedName":"localhost/sys/x$ps_schema_table_statistics_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$ps_schema_table_statistics_io","FullyQualifiedName":"localhost/sys/x$ps_schema_table_statistics_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$ps_schema_table_statistics_io","FullyQualifiedName":"localhost/sys/x$ps_schema_table_statistics_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$ps_schema_table_statistics_io","FullyQualifiedName":"localhost/sys/x$ps_schema_table_statistics_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$ps_schema_table_statistics_io","FullyQualifiedName":"localhost/sys/x$ps_schema_table_statistics_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$schema_flattened_keys","FullyQualifiedName":"localhost/sys/x$schema_flattened_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$schema_flattened_keys","FullyQualifiedName":"localhost/sys/x$schema_flattened_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$schema_flattened_keys","FullyQualifiedName":"localhost/sys/x$schema_flattened_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$schema_flattened_keys","FullyQualifiedName":"localhost/sys/x$schema_flattened_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$schema_flattened_keys","FullyQualifiedName":"localhost/sys/x$schema_flattened_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$schema_flattened_keys","FullyQualifiedName":"localhost/sys/x$schema_flattened_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$schema_flattened_keys","FullyQualifiedName":"localhost/sys/x$schema_flattened_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$schema_flattened_keys","FullyQualifiedName":"localhost/sys/x$schema_flattened_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$schema_flattened_keys","FullyQualifiedName":"localhost/sys/x$schema_flattened_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$schema_flattened_keys","FullyQualifiedName":"localhost/sys/x$schema_flattened_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$schema_flattened_keys","FullyQualifiedName":"localhost/sys/x$schema_flattened_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$schema_flattened_keys","FullyQualifiedName":"localhost/sys/x$schema_flattened_keys","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$schema_index_statistics","FullyQualifiedName":"localhost/sys/x$schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$schema_index_statistics","FullyQualifiedName":"localhost/sys/x$schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$schema_index_statistics","FullyQualifiedName":"localhost/sys/x$schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$schema_index_statistics","FullyQualifiedName":"localhost/sys/x$schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$schema_index_statistics","FullyQualifiedName":"localhost/sys/x$schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$schema_index_statistics","FullyQualifiedName":"localhost/sys/x$schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$schema_index_statistics","FullyQualifiedName":"localhost/sys/x$schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$schema_index_statistics","FullyQualifiedName":"localhost/sys/x$schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$schema_index_statistics","FullyQualifiedName":"localhost/sys/x$schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$schema_index_statistics","FullyQualifiedName":"localhost/sys/x$schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$schema_index_statistics","FullyQualifiedName":"localhost/sys/x$schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$schema_index_statistics","FullyQualifiedName":"localhost/sys/x$schema_index_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$schema_table_lock_waits","FullyQualifiedName":"localhost/sys/x$schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$schema_table_lock_waits","FullyQualifiedName":"localhost/sys/x$schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$schema_table_lock_waits","FullyQualifiedName":"localhost/sys/x$schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$schema_table_lock_waits","FullyQualifiedName":"localhost/sys/x$schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$schema_table_lock_waits","FullyQualifiedName":"localhost/sys/x$schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$schema_table_lock_waits","FullyQualifiedName":"localhost/sys/x$schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$schema_table_lock_waits","FullyQualifiedName":"localhost/sys/x$schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$schema_table_lock_waits","FullyQualifiedName":"localhost/sys/x$schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$schema_table_lock_waits","FullyQualifiedName":"localhost/sys/x$schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$schema_table_lock_waits","FullyQualifiedName":"localhost/sys/x$schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$schema_table_lock_waits","FullyQualifiedName":"localhost/sys/x$schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$schema_table_lock_waits","FullyQualifiedName":"localhost/sys/x$schema_table_lock_waits","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics","FullyQualifiedName":"localhost/sys/x$schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics","FullyQualifiedName":"localhost/sys/x$schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics","FullyQualifiedName":"localhost/sys/x$schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics","FullyQualifiedName":"localhost/sys/x$schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics","FullyQualifiedName":"localhost/sys/x$schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics","FullyQualifiedName":"localhost/sys/x$schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics","FullyQualifiedName":"localhost/sys/x$schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics","FullyQualifiedName":"localhost/sys/x$schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics","FullyQualifiedName":"localhost/sys/x$schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics","FullyQualifiedName":"localhost/sys/x$schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics","FullyQualifiedName":"localhost/sys/x$schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics","FullyQualifiedName":"localhost/sys/x$schema_table_statistics","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/x$schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/x$schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/x$schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/x$schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/x$schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/x$schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/x$schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/x$schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/x$schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/x$schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/x$schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$schema_table_statistics_with_buffer","FullyQualifiedName":"localhost/sys/x$schema_table_statistics_with_buffer","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$schema_tables_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$schema_tables_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$session","FullyQualifiedName":"localhost/sys/x$session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$session","FullyQualifiedName":"localhost/sys/x$session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$session","FullyQualifiedName":"localhost/sys/x$session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$session","FullyQualifiedName":"localhost/sys/x$session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$session","FullyQualifiedName":"localhost/sys/x$session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$session","FullyQualifiedName":"localhost/sys/x$session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$session","FullyQualifiedName":"localhost/sys/x$session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$session","FullyQualifiedName":"localhost/sys/x$session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$session","FullyQualifiedName":"localhost/sys/x$session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$session","FullyQualifiedName":"localhost/sys/x$session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$session","FullyQualifiedName":"localhost/sys/x$session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$session","FullyQualifiedName":"localhost/sys/x$session","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$statement_analysis","FullyQualifiedName":"localhost/sys/x$statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$statement_analysis","FullyQualifiedName":"localhost/sys/x$statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$statement_analysis","FullyQualifiedName":"localhost/sys/x$statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$statement_analysis","FullyQualifiedName":"localhost/sys/x$statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$statement_analysis","FullyQualifiedName":"localhost/sys/x$statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$statement_analysis","FullyQualifiedName":"localhost/sys/x$statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$statement_analysis","FullyQualifiedName":"localhost/sys/x$statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$statement_analysis","FullyQualifiedName":"localhost/sys/x$statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$statement_analysis","FullyQualifiedName":"localhost/sys/x$statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$statement_analysis","FullyQualifiedName":"localhost/sys/x$statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$statement_analysis","FullyQualifiedName":"localhost/sys/x$statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$statement_analysis","FullyQualifiedName":"localhost/sys/x$statement_analysis","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/x$statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/x$statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/x$statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/x$statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/x$statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/x$statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/x$statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/x$statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/x$statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/x$statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/x$statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$statements_with_errors_or_warnings","FullyQualifiedName":"localhost/sys/x$statements_with_errors_or_warnings","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$statements_with_full_table_scans","FullyQualifiedName":"localhost/sys/x$statements_with_full_table_scans","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/x$statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/x$statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/x$statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/x$statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/x$statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/x$statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/x$statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/x$statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/x$statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/x$statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/x$statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$statements_with_runtimes_in_95th_percentile","FullyQualifiedName":"localhost/sys/x$statements_with_runtimes_in_95th_percentile","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$statements_with_sorting","FullyQualifiedName":"localhost/sys/x$statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$statements_with_sorting","FullyQualifiedName":"localhost/sys/x$statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$statements_with_sorting","FullyQualifiedName":"localhost/sys/x$statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$statements_with_sorting","FullyQualifiedName":"localhost/sys/x$statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$statements_with_sorting","FullyQualifiedName":"localhost/sys/x$statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$statements_with_sorting","FullyQualifiedName":"localhost/sys/x$statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$statements_with_sorting","FullyQualifiedName":"localhost/sys/x$statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$statements_with_sorting","FullyQualifiedName":"localhost/sys/x$statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$statements_with_sorting","FullyQualifiedName":"localhost/sys/x$statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$statements_with_sorting","FullyQualifiedName":"localhost/sys/x$statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$statements_with_sorting","FullyQualifiedName":"localhost/sys/x$statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$statements_with_sorting","FullyQualifiedName":"localhost/sys/x$statements_with_sorting","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$statements_with_temp_tables","FullyQualifiedName":"localhost/sys/x$statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$statements_with_temp_tables","FullyQualifiedName":"localhost/sys/x$statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$statements_with_temp_tables","FullyQualifiedName":"localhost/sys/x$statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$statements_with_temp_tables","FullyQualifiedName":"localhost/sys/x$statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$statements_with_temp_tables","FullyQualifiedName":"localhost/sys/x$statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$statements_with_temp_tables","FullyQualifiedName":"localhost/sys/x$statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$statements_with_temp_tables","FullyQualifiedName":"localhost/sys/x$statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$statements_with_temp_tables","FullyQualifiedName":"localhost/sys/x$statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$statements_with_temp_tables","FullyQualifiedName":"localhost/sys/x$statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$statements_with_temp_tables","FullyQualifiedName":"localhost/sys/x$statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$statements_with_temp_tables","FullyQualifiedName":"localhost/sys/x$statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$statements_with_temp_tables","FullyQualifiedName":"localhost/sys/x$statements_with_temp_tables","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$user_summary","FullyQualifiedName":"localhost/sys/x$user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$user_summary","FullyQualifiedName":"localhost/sys/x$user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$user_summary","FullyQualifiedName":"localhost/sys/x$user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$user_summary","FullyQualifiedName":"localhost/sys/x$user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$user_summary","FullyQualifiedName":"localhost/sys/x$user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$user_summary","FullyQualifiedName":"localhost/sys/x$user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$user_summary","FullyQualifiedName":"localhost/sys/x$user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$user_summary","FullyQualifiedName":"localhost/sys/x$user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$user_summary","FullyQualifiedName":"localhost/sys/x$user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$user_summary","FullyQualifiedName":"localhost/sys/x$user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$user_summary","FullyQualifiedName":"localhost/sys/x$user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$user_summary","FullyQualifiedName":"localhost/sys/x$user_summary","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$user_summary_by_file_io_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_file_io_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$user_summary_by_stages","FullyQualifiedName":"localhost/sys/x$user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$user_summary_by_stages","FullyQualifiedName":"localhost/sys/x$user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$user_summary_by_stages","FullyQualifiedName":"localhost/sys/x$user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$user_summary_by_stages","FullyQualifiedName":"localhost/sys/x$user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$user_summary_by_stages","FullyQualifiedName":"localhost/sys/x$user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$user_summary_by_stages","FullyQualifiedName":"localhost/sys/x$user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$user_summary_by_stages","FullyQualifiedName":"localhost/sys/x$user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$user_summary_by_stages","FullyQualifiedName":"localhost/sys/x$user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$user_summary_by_stages","FullyQualifiedName":"localhost/sys/x$user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$user_summary_by_stages","FullyQualifiedName":"localhost/sys/x$user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$user_summary_by_stages","FullyQualifiedName":"localhost/sys/x$user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$user_summary_by_stages","FullyQualifiedName":"localhost/sys/x$user_summary_by_stages","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_latency","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$user_summary_by_statement_type","FullyQualifiedName":"localhost/sys/x$user_summary_by_statement_type","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_avg_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_avg_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$wait_classes_global_by_latency","FullyQualifiedName":"localhost/sys/x$wait_classes_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$waits_by_host_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_host_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$waits_by_user_by_latency","FullyQualifiedName":"localhost/sys/x$waits_by_user_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}},{"Resource":{"Name":"x$waits_global_by_latency","FullyQualifiedName":"localhost/sys/x$waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"ALTER","Parent":null}},{"Resource":{"Name":"x$waits_global_by_latency","FullyQualifiedName":"localhost/sys/x$waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE","Parent":null}},{"Resource":{"Name":"x$waits_global_by_latency","FullyQualifiedName":"localhost/sys/x$waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"CREATE VIEW","Parent":null}},{"Resource":{"Name":"x$waits_global_by_latency","FullyQualifiedName":"localhost/sys/x$waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DELETE","Parent":null}},{"Resource":{"Name":"x$waits_global_by_latency","FullyQualifiedName":"localhost/sys/x$waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"DROP","Parent":null}},{"Resource":{"Name":"x$waits_global_by_latency","FullyQualifiedName":"localhost/sys/x$waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INDEX","Parent":null}},{"Resource":{"Name":"x$waits_global_by_latency","FullyQualifiedName":"localhost/sys/x$waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"INSERT","Parent":null}},{"Resource":{"Name":"x$waits_global_by_latency","FullyQualifiedName":"localhost/sys/x$waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"REFERENCES","Parent":null}},{"Resource":{"Name":"x$waits_global_by_latency","FullyQualifiedName":"localhost/sys/x$waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SELECT","Parent":null}},{"Resource":{"Name":"x$waits_global_by_latency","FullyQualifiedName":"localhost/sys/x$waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"SHOW VIEW","Parent":null}},{"Resource":{"Name":"x$waits_global_by_latency","FullyQualifiedName":"localhost/sys/x$waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"TRIGGER","Parent":null}},{"Resource":{"Name":"x$waits_global_by_latency","FullyQualifiedName":"localhost/sys/x$waits_global_by_latency","Type":"table","Metadata":{"bytes":0,"non_existent":false},"Parent":{"Name":"sys","FullyQualifiedName":"localhost/sys","Type":"database","Metadata":{"default":true,"non_existent":false},"Parent":{"Name":"root@%","FullyQualifiedName":"localhost/root@%","Type":"user","Metadata":null,"Parent":null}}},"Permission":{"Value":"UPDATE","Parent":null}}],"UnboundedResources":null,"Metadata":null}
================================================
FILE: pkg/analyzer/analyzers/mysql/mysql.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go mysql
package mysql
import (
"database/sql"
"fmt"
"os"
"strings"
"time"
"github.com/dustin/go-humanize"
_ "github.com/go-sql-driver/mysql"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/xo/dburl"
"github.com/fatih/color"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeMySQL }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
uri, ok := credInfo["connection_string"]
if !ok {
return nil, fmt.Errorf("missing connection string")
}
info, err := AnalyzePermissions(a.Cfg, uri)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeMySQL,
Metadata: nil,
Bindings: []analyzers.Binding{},
}
// add user privileges to bindings
userBindings, userResource := bakeUserBindings(info)
result.Bindings = append(result.Bindings, userBindings...)
// add user's database privileges to bindings
databaseBindings := bakeDatabaseBindings(userResource, info)
result.Bindings = append(result.Bindings, databaseBindings...)
return &result
}
func bakeUserBindings(info *SecretInfo) ([]analyzers.Binding, *analyzers.Resource) {
var userBindings []analyzers.Binding
// add user and their privileges to bindings
userResource := analyzers.Resource{
Name: info.User,
FullyQualifiedName: info.Host + "/" + info.User,
Type: "user",
}
for _, priv := range info.GlobalPrivs.Privs {
userBindings = append(userBindings, analyzers.Binding{
Resource: userResource,
Permission: analyzers.Permission{
Value: priv,
},
})
}
return userBindings, &userResource
}
func bakeDatabaseBindings(userResource *analyzers.Resource, info *SecretInfo) []analyzers.Binding {
var databaseBindings []analyzers.Binding
for _, database := range info.Databases {
dbResource := analyzers.Resource{
Name: database.Name,
FullyQualifiedName: info.Host + "/" + database.Name,
Type: "database",
Metadata: map[string]any{
"default": database.Default,
"non_existent": database.Nonexistent,
},
Parent: userResource,
}
for _, priv := range database.Privs {
databaseBindings = append(databaseBindings, analyzers.Binding{
Resource: dbResource,
Permission: analyzers.Permission{
Value: priv,
},
})
}
// add this database's table privileges to bindings
tableBindings := bakeTableBindings(&dbResource, database)
databaseBindings = append(databaseBindings, tableBindings...)
// add this database's routines privileges to bindings
routineBindings := bakeRoutineBindings(&dbResource, database)
databaseBindings = append(databaseBindings, routineBindings...)
}
return databaseBindings
}
func bakeTableBindings(dbResource *analyzers.Resource, database *Database) []analyzers.Binding {
if database.Tables == nil {
return nil
}
var tableBindings []analyzers.Binding
for _, table := range *database.Tables {
tableResource := analyzers.Resource{
Name: table.Name,
FullyQualifiedName: dbResource.FullyQualifiedName + "/" + table.Name,
Type: "table",
Metadata: map[string]any{
"bytes": table.Bytes,
"non_existent": table.Nonexistent,
},
Parent: dbResource,
}
for _, priv := range table.Privs {
tableBindings = append(tableBindings, analyzers.Binding{
Resource: tableResource,
Permission: analyzers.Permission{
Value: priv,
},
})
}
// Add this table's column privileges to bindings
for _, column := range table.Columns {
columnResource := analyzers.Resource{
Name: column.Name,
FullyQualifiedName: tableResource.FullyQualifiedName + "/" + column.Name,
Type: "column",
Parent: &tableResource,
}
for _, priv := range column.Privs {
tableBindings = append(tableBindings, analyzers.Binding{
Resource: columnResource,
Permission: analyzers.Permission{
Value: priv,
},
})
}
}
}
return tableBindings
}
func bakeRoutineBindings(dbResource *analyzers.Resource, database *Database) []analyzers.Binding {
if database.Routines == nil {
return nil
}
var routineBindings []analyzers.Binding
for _, routine := range *database.Routines {
routineResource := analyzers.Resource{
Name: routine.Name,
FullyQualifiedName: dbResource.FullyQualifiedName + "/" + routine.Name,
Type: "routine",
Metadata: map[string]any{
"non_existent": routine.Nonexistent,
},
Parent: dbResource,
}
for _, priv := range routine.Privs {
routineBindings = append(routineBindings, analyzers.Binding{
Resource: routineResource,
Permission: analyzers.Permission{
Value: priv,
},
})
}
}
return routineBindings
}
const (
// MySQL SSL Modes
mysql_sslmode = "ssl-mode"
mysql_sslmode_disabled = "DISABLED"
mysql_sslmode_preferred = "PREFERRED"
mysql_sslmode_required = "REQUIRED"
mysql_sslmode_verify_ca = "VERIFY_CA"
mysql_sslmode_verify_identity = "VERIFY_IDENTITY"
//https://github.com/go-sql-driver/mysql/issues/899#issuecomment-443493840
// MySQL Built-in Databases
mysql_db_sys = "sys"
mysql_db_perf_sch = "performance_schema"
mysql_db_info_sch = "information_schema"
mysql_db_mysql = "mysql"
mysql_all = "*"
)
type GlobalPrivs struct {
Privs []string
}
type Database struct {
Name string
Default bool
Tables *[]Table
Privs []string
Routines *[]Routine
Nonexistent bool
}
type Table struct {
Name string
Columns []Column
Privs []string
Nonexistent bool
Bytes uint64
}
type Column struct {
Name string
Privs []string
}
type Routine struct {
Name string
Privs []string
Nonexistent bool
}
// so CURRENT_USER returns `doadmin@%` and not `doadmin@localhost
// USER() returns `doadmin@localhost`
type SecretInfo struct {
Host string
User string
Databases map[string]*Database
GlobalPrivs GlobalPrivs
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
// ToDo: Add in logging
if cfg.LoggingEnabled {
color.Red("[x] Logging is not supported for this analyzer.")
return
}
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
color.Green("[+] Successfully connected as user: %s", info.User)
// Print the results
printResults(info.Databases, info.GlobalPrivs, cfg.ShowAll)
}
func AnalyzePermissions(cfg *config.Config, connectionStr string) (*SecretInfo, error) {
// Parse the connection string
u, err := parseConnectionStr(connectionStr)
if err != nil {
return nil, fmt.Errorf("parsing the connection string: %w", err)
}
db, err := createConnection(u)
if err != nil {
return nil, fmt.Errorf("connecting to the MySQL database: %w", err)
}
defer db.Close()
// Get the current user
user, err := getUser(db)
if err != nil {
return nil, fmt.Errorf("getting the current user: %w", err)
}
// Get all accessible databases
var databases = make(map[string]*Database, 0)
err = getDatabases(db, databases)
if err != nil {
return nil, fmt.Errorf("getting databases: %w", err)
}
//Get all accessible tables
err = getTables(db, databases)
if err != nil {
return nil, fmt.Errorf("getting tables: %w", err)
}
// Get user grants
grants, err := getGrants(db)
if err != nil {
return nil, fmt.Errorf("getting user grants: %w", err)
}
// Get all accessible routines
err = getRoutines(db, databases)
if err != nil {
return nil, fmt.Errorf("getting routines: %w", err)
}
var globalPrivs GlobalPrivs
// Process user grants
processGrants(grants, databases, &globalPrivs)
return &SecretInfo{
Host: u.Hostname(),
User: user,
Databases: databases,
GlobalPrivs: globalPrivs,
}, nil
}
func parseConnectionStr(connection string) (*dburl.URL, error) {
// Check if the connection string starts with 'mysql://'
if !strings.HasPrefix(connection, "mysql://") {
color.Yellow("[i] The connection string should start with 'mysql://'. Adding it for you.")
connection = "mysql://" + connection
}
// Adapt ssl-mode params to Go MySQL driver
connection, err := fixTLSQueryParam(connection)
if err != nil {
return nil, err
}
// Parse the connection string
u, err := dburl.Parse(connection)
if err != nil {
return nil, err
}
return u, nil
}
func createConnection(u *dburl.URL) (*sql.DB, error) {
// Connect to the MySQL database
db, err := sql.Open("mysql", u.DSN)
if err != nil {
return nil, err
}
db.SetConnMaxLifetime(time.Minute * 5)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
// Check the connection
err = db.Ping()
if err != nil {
if strings.Contains(err.Error(), "certificate signed by unknown authority") {
return nil, fmt.Errorf("%s. try adding 'ssl-mode=PREFERRED' to your connection string", err.Error())
}
return nil, err
}
return db, nil
}
func fixTLSQueryParam(connection string) (string, error) {
// Parse connection string on "?"
parsed := strings.Split(connection, "?")
// Check if has query parms
if len(parsed) < 2 {
// Add 10s timeout
connection += "?timeout=10s"
return connection, nil
}
var error error
// Split parms
querySlice := strings.Split(parsed[1], "&")
// Check if ssl-mode is present
for i, part := range querySlice {
if strings.HasPrefix(part, "ssl-mode") {
mode := strings.Split(part, "=")[1]
switch mode {
case mysql_sslmode_disabled:
querySlice[i] = "tls=false"
case mysql_sslmode_preferred:
querySlice[i] = "tls=preferred"
case mysql_sslmode_required:
querySlice[i] = "tls=true"
case mysql_sslmode_verify_ca:
error = fmt.Errorf("this implementation does not support VERIFY_CA. try removing it or using ssl-mode=REQUIRED")
// Need to implement --ssl-ca or --ssl-capath
case mysql_sslmode_verify_identity:
error = fmt.Errorf("this implementation does not support VERIFY_IDENTITY. try removing it or using ssl-mode=REQUIRED")
// Need to implement --ssl-ca or --ssl-capath
}
}
}
// Join the parts back together
newQuerySlice := strings.Join(querySlice, "&")
return (parsed[0] + "?" + newQuerySlice + "&timeout=10s"), error
}
func getUser(db *sql.DB) (string, error) {
var user string
err := db.QueryRow("SELECT CURRENT_USER()").Scan(&user)
if err != nil {
return "", err
}
return user, nil
}
func getDatabases(db *sql.DB, databases map[string]*Database) error {
rows, err := db.Query("SHOW DATABASES")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var dbName string
err = rows.Scan(&dbName)
if err != nil {
return err
}
// check if the database is a built-in database
built_in_db := false
switch dbName {
case mysql_db_sys, mysql_db_perf_sch, mysql_db_info_sch, mysql_db_mysql:
built_in_db = true
}
// add the database to the databases map
newTables := make([]Table, 0)
newRoutines := make([]Routine, 0)
databases[dbName] = &Database{Name: dbName, Default: built_in_db, Tables: &newTables, Routines: &newRoutines}
}
return nil
}
func getTables(db *sql.DB, databases map[string]*Database) error {
rows, err := db.Query("SELECT table_schema, table_name, IFNULL(DATA_LENGTH,0) FROM information_schema.tables")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var dbName string
var tableName string
var tableSize uint64
err = rows.Scan(&dbName, &tableName, &tableSize)
if err != nil {
return err
}
// find the database in the databases slice
d := databases[dbName]
*d.Tables = append(*d.Tables, Table{Name: tableName, Bytes: tableSize})
}
return nil
}
func getRoutines(db *sql.DB, databases map[string]*Database) error {
rows, err := db.Query("SELECT routine_schema, routine_name FROM information_schema.routines")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var dbName string
var routineName string
err = rows.Scan(&dbName, &routineName)
if err != nil {
return err
}
// find the database in the databases slice
d, ok := databases[dbName]
if !ok {
databases[dbName] = &Database{Name: dbName, Default: false, Tables: &[]Table{}, Routines: &[]Routine{}, Nonexistent: true}
d = databases[dbName]
}
*d.Routines = append(*d.Routines, Routine{Name: routineName})
}
return nil
}
func getGrants(db *sql.DB) ([]string, error) {
rows, err := db.Query("SHOW GRANTS")
if err != nil {
return nil, err
}
defer rows.Close()
var grants []string
for rows.Next() {
var grant string
err = rows.Scan(&grant)
if err != nil {
return nil, err
}
grants = append(grants, grant)
}
return grants, nil
}
// ToDo: Deal with these GRANT/REVOKE statements
// GRANT SELECT (col1), INSERT (col1, col2) ON mydb.mytbl TO 'someuser'@'somehost';
// GRANT PROXY ON 'localuser'@'localhost' TO 'externaluser'@'somehost';
// GRANT 'role1', 'role2' TO 'user1'@'localhost', 'user2'@'localhost';
// What are the default privs on information_schema and performance_Schema?
// Seems table by table...maybe just put "Not Implemented" and leave this to be a show_all option.
// Note: Can't GRANT on a table that doesn't exist, but DB is fine.
// processGrants processes the grants and adds them to the databases structs and globalPrivs
func processGrants(grants []string, databases map[string]*Database, globalPrivs *GlobalPrivs) {
for _, grant := range grants {
// GRANTs on non-existent databases are valid, but we need that object to exist in "databases" for processGrant().
db := parseDBFromGrant(grant)
if db == mysql_all {
continue
}
_, ok := databases[db]
if !ok {
databases[db] = &Database{Name: db, Default: false, Tables: &[]Table{}, Routines: &[]Routine{}, Nonexistent: true}
}
}
for _, grant := range grants {
// TODO: How to deal with error here?
_ = processGrant(grant, databases, globalPrivs)
}
}
func processGrant(grant string, databases map[string]*Database, globalPrivs *GlobalPrivs) error {
isGrant := strings.HasPrefix(grant, "GRANT")
//hasGrantOption := strings.HasSuffix(grant, "WITH GRANT OPTION")
// remove GRANT or REVOKE
grant = strings.TrimPrefix(grant, "GRANT")
grant = strings.TrimPrefix(grant, "REVOKE")
// Split on " ON "
parts := strings.Split(grant, " ON ")
if len(parts) < 2 {
return fmt.Errorf("Error processing grant: %s", grant)
}
// Put privs in a slice
privs := strings.Split(parts[0], ",")
for i, priv := range privs {
privs[i] = strings.Trim(priv, " ")
}
// Get DB and Table
dbName := strings.Trim(strings.Split(parts[1], " TO ")[0], " ")
if dbName == parts[1] {
dbName = strings.Trim(strings.Split(parts[1], " FROM ")[0], " ")
}
// Find the database in the databases slice
// Note: table may not exist yet OR may be a routine
dbTableParts := strings.Split(dbName, ".")
db := strings.Trim(dbTableParts[0], "\"`")
table := strings.Trim(dbTableParts[1], "\"`")
// dont' forget to deal with revoking db-level privs
if db == mysql_all {
// Deal with "ALL" and "ALL PRIVILEGES"
switch privs[0] {
case "ALL", "ALL PRIVILEGES":
addRemoveAllPrivs(databases, globalPrivs, isGrant)
default:
for _, priv := range privs {
addRemoveOnePrivOnAll(databases, globalPrivs, priv, isGrant)
}
}
} else {
// Check if the privs are for a routine
isRoutine := checkIsRoutine(privs)
if isRoutine {
db = strings.TrimPrefix(db, "PROCEDURE `")
db = strings.TrimSuffix(db, "`")
}
d := databases[db]
switch {
case table == mysql_all:
filteredDBPrivs := filterDBPrivs(privs)
filteredTablePrivs := filterTablePrivs(privs)
d.Privs = addRemovePrivs(d.Privs, filteredDBPrivs, isGrant)
for i, t := range *d.Tables {
(*d.Tables)[i].Privs = addRemovePrivs(t.Privs, filteredTablePrivs, isGrant)
}
case isRoutine:
var idx = getRoutineIndex(d, table)
if idx == -1 {
*d.Routines = append(*d.Routines, Routine{Name: table, Nonexistent: true})
idx = len(*d.Routines) - 1
}
(*d.Routines)[idx].Privs = addRemovePrivs((*d.Routines)[idx].Privs, privs, isGrant)
default:
var idx = getTableIndex(d, table)
if idx == -1 {
*d.Tables = append(*d.Tables, Table{Name: table, Nonexistent: true, Bytes: 0})
idx = len(*d.Tables) - 1
}
(*d.Tables)[idx].Privs = addRemovePrivs((*d.Tables)[idx].Privs, privs, isGrant)
}
}
return nil
}
func parseDBFromGrant(grant string) string {
// Split on " ON "
parts := strings.Split(grant, " ON ")
if len(parts) < 2 {
color.Red("[!] Error processing grant: %s", grant)
return ""
}
// Get DB and Table
dbName := strings.Trim(strings.Split(parts[1], " TO ")[0], " ")
if dbName == parts[1] {
dbName = strings.Trim(strings.Split(parts[1], " FROM ")[0], " ")
}
dbTableParts := strings.Split(dbName, ".")
db := strings.Trim(dbTableParts[0], "\"`")
db = strings.TrimPrefix(db, "PROCEDURE `")
db = strings.TrimSuffix(db, "`")
return db
}
func filterDBPrivs(privs []string) []string {
filtered := make([]string, 0)
for _, priv := range privs {
if SCOPES[priv].Database {
filtered = append(filtered, priv)
}
}
return filtered
}
func filterTablePrivs(privs []string) []string {
filtered := make([]string, 0)
for _, priv := range privs {
if SCOPES[priv].Table {
filtered = append(filtered, priv)
}
}
return filtered
}
func addRemoveOnePrivOnAll(databases map[string]*Database, globalPrivs *GlobalPrivs, priv string, isGrant bool) {
scope, ok := SCOPES[priv]
if !ok {
color.Red("[!] Error processing grant: privilege doesn't exist in our MySQL (%s)", priv)
return
}
slicedPriv := []string{priv}
// Add priv to globalPrivs
if scope.Global {
globalPrivs.Privs = addRemovePrivs(globalPrivs.Privs, slicedPriv, isGrant)
}
// Add/Remove priv to all databases
if scope.Database {
for _, d := range databases {
if d.Name == "information_schema" || d.Name == "performance_schema" {
continue
}
d.Privs = addRemovePrivs(d.Privs, slicedPriv, isGrant)
}
}
// Add/Remove priv to all tables
if scope.Table {
for _, d := range databases {
for i, t := range *d.Tables {
(*d.Tables)[i].Privs = addRemovePrivs(t.Privs, slicedPriv, isGrant)
}
}
}
// Add/Remove priv to all routines
if scope.Routine {
for _, d := range databases {
for i, r := range *d.Routines {
(*d.Routines)[i].Privs = addRemovePrivs(r.Privs, slicedPriv, isGrant)
}
}
}
}
func addRemoveAllPrivs(databases map[string]*Database, globalPrivs *GlobalPrivs, isGrant bool) {
// Add all privs to globalPrivs
globalAllPrivs := getGlobalAllPrivileges()
globalPrivs.Privs = addRemovePrivs(globalPrivs.Privs, globalAllPrivs, isGrant)
// Get DB, Table and Routine Privs
dbAllPrivs := getDBAllPrivs()
tableAllPrivs := getTableAllPrivs()
routineAllPrivs := getRoutineAllPrivs()
// Add all privs to all databases and tables and routines
for _, d := range databases {
if d.Name == "information_schema" || d.Name == "performance_schema" {
continue
}
// Add DB-level privs
d.Privs = addRemovePrivs(d.Privs, dbAllPrivs, isGrant)
// Add Table-level privs
for i, t := range *d.Tables {
(*d.Tables)[i].Privs = addRemovePrivs(t.Privs, tableAllPrivs, isGrant)
}
// Add Routine-level privs
for i, r := range *d.Routines {
(*d.Routines)[i].Privs = addRemovePrivs(r.Privs, routineAllPrivs, isGrant)
}
}
}
func getGlobalAllPrivileges() []string {
privs := make([]string, 0)
for priv, scope := range SCOPES {
if scope.Global && !scope.Dynamic && priv != "USAGE" && priv != "GRANT OPTION" {
privs = append(privs, priv)
}
}
return privs
}
func getDBAllPrivs() []string {
privs := make([]string, 0)
for priv, scope := range SCOPES {
if scope.Database && !scope.Dynamic && priv != "USAGE" && priv != "GRANT OPTION" {
privs = append(privs, priv)
}
}
return privs
}
func getTableAllPrivs() []string {
privs := make([]string, 0)
for priv, scope := range SCOPES {
if scope.Table && !scope.Dynamic && priv != "USAGE" && priv != "GRANT OPTION" {
privs = append(privs, priv)
}
}
return privs
}
func getRoutineAllPrivs() []string {
privs := make([]string, 0)
for priv, scope := range SCOPES {
if scope.Routine && !scope.Dynamic && priv != "USAGE" && priv != "GRANT OPTION" {
privs = append(privs, priv)
}
}
return privs
}
func checkIsRoutine(privs []string) bool {
if len(privs) > 0 {
return SCOPES[privs[0]].Routine
}
return false
}
func getTableIndex(d *Database, tableName string) int {
for i, t := range *d.Tables {
if t.Name == tableName {
return i
}
}
return -1
}
func getRoutineIndex(d *Database, routineName string) int {
for i, r := range *d.Routines {
if r.Name == routineName {
return i
}
}
return -1
}
func addRemovePrivs(currentPrivs []string, privsToAddRemove []string, add bool) []string {
newPrivs := make([]string, 0)
if add {
newPrivs = append(currentPrivs, privsToAddRemove...)
return newPrivs
}
for _, p := range currentPrivs {
found := false
for _, p2 := range privsToAddRemove {
if p == p2 {
found = true
break
}
}
if !found {
newPrivs = append(newPrivs, p)
}
}
return newPrivs
}
func printResults(databases map[string]*Database, globalPrivs GlobalPrivs, showAll bool) {
// Print Global Privileges
printGlobalPrivs(globalPrivs)
// Print Database and Table Privileges
printDBTablePrivs(databases, showAll)
// Print Routine Privileges
printRoutinePrivs(databases, showAll)
}
func printGlobalPrivs(globalPrivs GlobalPrivs) {
// Prep table writer
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Global Privileges"})
// Print global privs
globalPrivsStr := ""
for _, priv := range globalPrivs.Privs {
globalPrivsStr += priv + ", "
}
// Clean up privs string
globalPrivsStr = cleanPrivStr(globalPrivsStr)
// Add rows of priv string data
t.AppendRow([]interface{}{analyzers.GreenWriter(text.WrapSoft(globalPrivsStr, 100))})
t.Render()
}
func printDBTablePrivs(databases map[string]*Database, showAll bool) {
// Prep table writer
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Database", "Table", "Privileges", "Est. Size"})
// Print database privs
for _, d := range databases {
if isBuiltIn(d.Name) && !showAll {
continue
}
// Add privileges to db or table privs strings
dbPrivsStr := ""
dbTablesStr := ""
for _, priv := range d.Privs {
scope := SCOPES[priv]
if scope.Database && scope.Table {
dbTablesStr += priv + ", "
} else {
dbPrivsStr += priv + ", "
}
}
// Clean up privs strings
dbPrivsStr = cleanPrivStr(dbPrivsStr)
dbTablesStr = cleanPrivStr(dbTablesStr)
// Prep String colors
var dbName string
var writer func(a ...interface{}) string
if d.Default {
dbName = d.Name + " (built-in)"
writer = analyzers.YellowWriter
} else if d.Nonexistent {
dbName = d.Name + " (nonexistent)"
writer = analyzers.RedWriter
} else {
dbName = d.Name
writer = analyzers.GreenWriter
}
// Prep Priv Strings
// Add rows of priv string data
t.AppendRow([]interface{}{writer(dbName), writer(""), writer(text.WrapSoft(dbPrivsStr, 80)), writer("-")})
t.AppendRow([]interface{}{"", writer(""), writer(text.WrapSoft(dbTablesStr, 80)), writer("-")})
// Print table privs
for _, t2 := range *d.Tables {
tablePrivsStr := ""
for _, priv := range t2.Privs {
tablePrivsStr += priv + ", "
}
tablePrivsStr = cleanPrivStr(tablePrivsStr)
t.AppendRow([]interface{}{"", writer(t2.Name), writer(text.WrapSoft(tablePrivsStr, 80)), writer(humanize.Bytes(t2.Bytes))})
}
// Add a separator between databases
t.AppendSeparator()
}
t.Render()
}
func printRoutinePrivs(databases map[string]*Database, showAll bool) {
// Print routine privs
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Database", "Routine", "Privileges"})
// Add rows of priv string data
for _, d := range databases {
if isBuiltIn(d.Name) && !showAll {
continue
}
for _, r := range *d.Routines {
routinePrivsStr := ""
for _, priv := range r.Privs {
routinePrivsStr += priv + ", "
}
routinePrivsStr = cleanPrivStr(routinePrivsStr)
var writer func(a ...interface{}) string
switch d.Name {
case mysql_db_info_sch, mysql_db_perf_sch, mysql_db_sys, mysql_db_mysql:
writer = analyzers.YellowWriter
default:
writer = analyzers.GreenWriter
}
t.AppendRow([]interface{}{writer(d.Name), writer(r.Name), writer(text.WrapSoft(routinePrivsStr, 80))})
}
}
t.Render()
}
func cleanPrivStr(priv string) string {
priv = strings.TrimSuffix(priv, ", ")
if priv == "" {
priv = "-"
}
return priv
}
func isBuiltIn(dbName string) bool {
switch dbName {
case mysql_db_sys, mysql_db_perf_sch, mysql_db_info_sch, mysql_db_mysql:
return true
}
return false
}
================================================
FILE: pkg/analyzer/analyzers/mysql/mysql_test.go
================================================
package mysql
import (
_ "embed"
"encoding/json"
"fmt"
"testing"
"github.com/brianvoe/gofakeit/v7"
"github.com/google/go-cmp/cmp"
"github.com/testcontainers/testcontainers-go/modules/mysql"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
mysqlUser := "root"
mysqlPass := gofakeit.Password(true, true, true, false, false, 10)
mysqlDatabase := "mysql"
ctx := context.Background()
mysqlC, err := mysql.Run(ctx, "mysql",
mysql.WithDatabase(mysqlDatabase),
mysql.WithUsername(mysqlUser),
mysql.WithPassword(mysqlPass),
)
if err != nil {
t.Fatal(err)
}
defer func() { _ = mysqlC.Terminate(ctx) }()
host, err := mysqlC.Host(ctx)
if err != nil {
t.Fatal(err)
}
port, err := mysqlC.MappedPort(ctx, "3306")
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
connectionString string
want []byte // JSON string
wantErr bool
}{
{
name: "valid Mysql connection",
connectionString: fmt.Sprintf(`root:%s@%s:%s/%s`, mysqlPass, host, port.Port(), mysqlDatabase),
want: expectedOutput,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(context.Background(), map[string]string{"connection_string": tt.connectionString})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal(tt.want, &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare bindings separately because they are not guaranteed to be in the same order
if len(got.Bindings) != len(wantObj.Bindings) {
t.Errorf("Analyzer.Analyze() = %s, want %s", gotJSON, wantJSON)
return
}
got.Bindings = nil
wantObj.Bindings = nil
// Compare the rest of the Object
if diff := cmp.Diff(&wantObj, got); diff != "" {
t.Errorf("%s: (-want +got)\n%s", tt.name, diff)
return
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/mysql/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package mysql
import "errors"
type Permission int
const (
Invalid Permission = iota
Alter Permission = iota
AlterRoutine Permission = iota
AllowNonexistentDefiner Permission = iota
ApplicationPasswordAdmin Permission = iota
AuditAbortExempt Permission = iota
AuditAdmin Permission = iota
AuthenticationPolicyAdmin Permission = iota
BackupAdmin Permission = iota
BinlogAdmin Permission = iota
BinlogEncryptionAdmin Permission = iota
CloneAdmin Permission = iota
ConnectionAdmin Permission = iota
Create Permission = iota
CreateRole Permission = iota
CreateRoutine Permission = iota
CreateTablespace Permission = iota
CreateTemporaryTables Permission = iota
CreateUser Permission = iota
CreateView Permission = iota
Delete Permission = iota
Drop Permission = iota
DropRole Permission = iota
EncryptionKeyAdmin Permission = iota
Event Permission = iota
Execute Permission = iota
File Permission = iota
FirewallAdmin Permission = iota
FirewallExempt Permission = iota
FirewallUser Permission = iota
FlushOptimizerCosts Permission = iota
FlushStatus Permission = iota
FlushTables Permission = iota
FlushUserResources Permission = iota
GrantOption Permission = iota
GroupReplicationAdmin Permission = iota
GroupReplicationStream Permission = iota
Index Permission = iota
InnodbRedoLogArchive Permission = iota
InnodbRedoLogEnable Permission = iota
Insert Permission = iota
LockingTables Permission = iota
MaskingDictionariesAdmin Permission = iota
NdbStoredUser Permission = iota
PasswordlessUserAdmin Permission = iota
PersistRoVariablesAdmin Permission = iota
Process Permission = iota
Proxy Permission = iota
References Permission = iota
Reload Permission = iota
ReplicationApplier Permission = iota
ReplicationClient Permission = iota
ReplicationSlave Permission = iota
ReplicationSlaveAdmin Permission = iota
ResourceGroupAdmin Permission = iota
ResourceGroupUser Permission = iota
RoleAdmin Permission = iota
Select Permission = iota
SensitiveVariablesObserver Permission = iota
ServiceConnectionAdmin Permission = iota
SessionVariablesAdmin Permission = iota
SetAnyDefiner Permission = iota
SetUserId Permission = iota
ShowDatabases Permission = iota
ShowRoutine Permission = iota
ShowView Permission = iota
Shutdown Permission = iota
SkipQueryRewrite Permission = iota
Super Permission = iota
SystemUser Permission = iota
SystemVariablesAdmin Permission = iota
TableEncryptionAdmin Permission = iota
TelemetryLogAdmin Permission = iota
TpConnectionAdmin Permission = iota
TransactionGtidTag Permission = iota
Trigger Permission = iota
Update Permission = iota
Usage Permission = iota
VersionTokenAdmin Permission = iota
XaRecoverAdmin Permission = iota
)
var (
PermissionStrings = map[Permission]string{
Alter: "alter",
AlterRoutine: "alter_routine",
AllowNonexistentDefiner: "allow_nonexistent_definer",
ApplicationPasswordAdmin: "application_password_admin",
AuditAbortExempt: "audit_abort_exempt",
AuditAdmin: "audit_admin",
AuthenticationPolicyAdmin: "authentication_policy_admin",
BackupAdmin: "backup_admin",
BinlogAdmin: "binlog_admin",
BinlogEncryptionAdmin: "binlog_encryption_admin",
CloneAdmin: "clone_admin",
ConnectionAdmin: "connection_admin",
Create: "create",
CreateRole: "create_role",
CreateRoutine: "create_routine",
CreateTablespace: "create_tablespace",
CreateTemporaryTables: "create_temporary_tables",
CreateUser: "create_user",
CreateView: "create_view",
Delete: "delete",
Drop: "drop",
DropRole: "drop_role",
EncryptionKeyAdmin: "encryption_key_admin",
Event: "event",
Execute: "execute",
File: "file",
FirewallAdmin: "firewall_admin",
FirewallExempt: "firewall_exempt",
FirewallUser: "firewall_user",
FlushOptimizerCosts: "flush_optimizer_costs",
FlushStatus: "flush_status",
FlushTables: "flush_tables",
FlushUserResources: "flush_user_resources",
GrantOption: "grant_option",
GroupReplicationAdmin: "group_replication_admin",
GroupReplicationStream: "group_replication_stream",
Index: "index",
InnodbRedoLogArchive: "innodb_redo_log_archive",
InnodbRedoLogEnable: "innodb_redo_log_enable",
Insert: "insert",
LockingTables: "locking_tables",
MaskingDictionariesAdmin: "masking_dictionaries_admin",
NdbStoredUser: "ndb_stored_user",
PasswordlessUserAdmin: "passwordless_user_admin",
PersistRoVariablesAdmin: "persist_ro_variables_admin",
Process: "process",
Proxy: "proxy",
References: "references",
Reload: "reload",
ReplicationApplier: "replication_applier",
ReplicationClient: "replication_client",
ReplicationSlave: "replication_slave",
ReplicationSlaveAdmin: "replication_slave_admin",
ResourceGroupAdmin: "resource_group_admin",
ResourceGroupUser: "resource_group_user",
RoleAdmin: "role_admin",
Select: "select",
SensitiveVariablesObserver: "sensitive_variables_observer",
ServiceConnectionAdmin: "service_connection_admin",
SessionVariablesAdmin: "session_variables_admin",
SetAnyDefiner: "set_any_definer",
SetUserId: "set_user_id",
ShowDatabases: "show_databases",
ShowRoutine: "show_routine",
ShowView: "show_view",
Shutdown: "shutdown",
SkipQueryRewrite: "skip_query_rewrite",
Super: "super",
SystemUser: "system_user",
SystemVariablesAdmin: "system_variables_admin",
TableEncryptionAdmin: "table_encryption_admin",
TelemetryLogAdmin: "telemetry_log_admin",
TpConnectionAdmin: "tp_connection_admin",
TransactionGtidTag: "transaction_gtid_tag",
Trigger: "trigger",
Update: "update",
Usage: "usage",
VersionTokenAdmin: "version_token_admin",
XaRecoverAdmin: "xa_recover_admin",
}
StringToPermission = map[string]Permission{
"alter": Alter,
"alter_routine": AlterRoutine,
"allow_nonexistent_definer": AllowNonexistentDefiner,
"application_password_admin": ApplicationPasswordAdmin,
"audit_abort_exempt": AuditAbortExempt,
"audit_admin": AuditAdmin,
"authentication_policy_admin": AuthenticationPolicyAdmin,
"backup_admin": BackupAdmin,
"binlog_admin": BinlogAdmin,
"binlog_encryption_admin": BinlogEncryptionAdmin,
"clone_admin": CloneAdmin,
"connection_admin": ConnectionAdmin,
"create": Create,
"create_role": CreateRole,
"create_routine": CreateRoutine,
"create_tablespace": CreateTablespace,
"create_temporary_tables": CreateTemporaryTables,
"create_user": CreateUser,
"create_view": CreateView,
"delete": Delete,
"drop": Drop,
"drop_role": DropRole,
"encryption_key_admin": EncryptionKeyAdmin,
"event": Event,
"execute": Execute,
"file": File,
"firewall_admin": FirewallAdmin,
"firewall_exempt": FirewallExempt,
"firewall_user": FirewallUser,
"flush_optimizer_costs": FlushOptimizerCosts,
"flush_status": FlushStatus,
"flush_tables": FlushTables,
"flush_user_resources": FlushUserResources,
"grant_option": GrantOption,
"group_replication_admin": GroupReplicationAdmin,
"group_replication_stream": GroupReplicationStream,
"index": Index,
"innodb_redo_log_archive": InnodbRedoLogArchive,
"innodb_redo_log_enable": InnodbRedoLogEnable,
"insert": Insert,
"locking_tables": LockingTables,
"masking_dictionaries_admin": MaskingDictionariesAdmin,
"ndb_stored_user": NdbStoredUser,
"passwordless_user_admin": PasswordlessUserAdmin,
"persist_ro_variables_admin": PersistRoVariablesAdmin,
"process": Process,
"proxy": Proxy,
"references": References,
"reload": Reload,
"replication_applier": ReplicationApplier,
"replication_client": ReplicationClient,
"replication_slave": ReplicationSlave,
"replication_slave_admin": ReplicationSlaveAdmin,
"resource_group_admin": ResourceGroupAdmin,
"resource_group_user": ResourceGroupUser,
"role_admin": RoleAdmin,
"select": Select,
"sensitive_variables_observer": SensitiveVariablesObserver,
"service_connection_admin": ServiceConnectionAdmin,
"session_variables_admin": SessionVariablesAdmin,
"set_any_definer": SetAnyDefiner,
"set_user_id": SetUserId,
"show_databases": ShowDatabases,
"show_routine": ShowRoutine,
"show_view": ShowView,
"shutdown": Shutdown,
"skip_query_rewrite": SkipQueryRewrite,
"super": Super,
"system_user": SystemUser,
"system_variables_admin": SystemVariablesAdmin,
"table_encryption_admin": TableEncryptionAdmin,
"telemetry_log_admin": TelemetryLogAdmin,
"tp_connection_admin": TpConnectionAdmin,
"transaction_gtid_tag": TransactionGtidTag,
"trigger": Trigger,
"update": Update,
"usage": Usage,
"version_token_admin": VersionTokenAdmin,
"xa_recover_admin": XaRecoverAdmin,
}
PermissionIDs = map[Permission]int{
Alter: 1,
AlterRoutine: 2,
AllowNonexistentDefiner: 3,
ApplicationPasswordAdmin: 4,
AuditAbortExempt: 5,
AuditAdmin: 6,
AuthenticationPolicyAdmin: 7,
BackupAdmin: 8,
BinlogAdmin: 9,
BinlogEncryptionAdmin: 10,
CloneAdmin: 11,
ConnectionAdmin: 12,
Create: 13,
CreateRole: 14,
CreateRoutine: 15,
CreateTablespace: 16,
CreateTemporaryTables: 17,
CreateUser: 18,
CreateView: 19,
Delete: 20,
Drop: 21,
DropRole: 22,
EncryptionKeyAdmin: 23,
Event: 24,
Execute: 25,
File: 26,
FirewallAdmin: 27,
FirewallExempt: 28,
FirewallUser: 29,
FlushOptimizerCosts: 30,
FlushStatus: 31,
FlushTables: 32,
FlushUserResources: 33,
GrantOption: 34,
GroupReplicationAdmin: 35,
GroupReplicationStream: 36,
Index: 37,
InnodbRedoLogArchive: 38,
InnodbRedoLogEnable: 39,
Insert: 40,
LockingTables: 41,
MaskingDictionariesAdmin: 42,
NdbStoredUser: 43,
PasswordlessUserAdmin: 44,
PersistRoVariablesAdmin: 45,
Process: 46,
Proxy: 47,
References: 48,
Reload: 49,
ReplicationApplier: 50,
ReplicationClient: 51,
ReplicationSlave: 52,
ReplicationSlaveAdmin: 53,
ResourceGroupAdmin: 54,
ResourceGroupUser: 55,
RoleAdmin: 56,
Select: 57,
SensitiveVariablesObserver: 58,
ServiceConnectionAdmin: 59,
SessionVariablesAdmin: 60,
SetAnyDefiner: 61,
SetUserId: 62,
ShowDatabases: 63,
ShowRoutine: 64,
ShowView: 65,
Shutdown: 66,
SkipQueryRewrite: 67,
Super: 68,
SystemUser: 69,
SystemVariablesAdmin: 70,
TableEncryptionAdmin: 71,
TelemetryLogAdmin: 72,
TpConnectionAdmin: 73,
TransactionGtidTag: 74,
Trigger: 75,
Update: 76,
Usage: 77,
VersionTokenAdmin: 78,
XaRecoverAdmin: 79,
}
IdToPermission = map[int]Permission{
1: Alter,
2: AlterRoutine,
3: AllowNonexistentDefiner,
4: ApplicationPasswordAdmin,
5: AuditAbortExempt,
6: AuditAdmin,
7: AuthenticationPolicyAdmin,
8: BackupAdmin,
9: BinlogAdmin,
10: BinlogEncryptionAdmin,
11: CloneAdmin,
12: ConnectionAdmin,
13: Create,
14: CreateRole,
15: CreateRoutine,
16: CreateTablespace,
17: CreateTemporaryTables,
18: CreateUser,
19: CreateView,
20: Delete,
21: Drop,
22: DropRole,
23: EncryptionKeyAdmin,
24: Event,
25: Execute,
26: File,
27: FirewallAdmin,
28: FirewallExempt,
29: FirewallUser,
30: FlushOptimizerCosts,
31: FlushStatus,
32: FlushTables,
33: FlushUserResources,
34: GrantOption,
35: GroupReplicationAdmin,
36: GroupReplicationStream,
37: Index,
38: InnodbRedoLogArchive,
39: InnodbRedoLogEnable,
40: Insert,
41: LockingTables,
42: MaskingDictionariesAdmin,
43: NdbStoredUser,
44: PasswordlessUserAdmin,
45: PersistRoVariablesAdmin,
46: Process,
47: Proxy,
48: References,
49: Reload,
50: ReplicationApplier,
51: ReplicationClient,
52: ReplicationSlave,
53: ReplicationSlaveAdmin,
54: ResourceGroupAdmin,
55: ResourceGroupUser,
56: RoleAdmin,
57: Select,
58: SensitiveVariablesObserver,
59: ServiceConnectionAdmin,
60: SessionVariablesAdmin,
61: SetAnyDefiner,
62: SetUserId,
63: ShowDatabases,
64: ShowRoutine,
65: ShowView,
66: Shutdown,
67: SkipQueryRewrite,
68: Super,
69: SystemUser,
70: SystemVariablesAdmin,
71: TableEncryptionAdmin,
72: TelemetryLogAdmin,
73: TpConnectionAdmin,
74: TransactionGtidTag,
75: Trigger,
76: Update,
77: Usage,
78: VersionTokenAdmin,
79: XaRecoverAdmin,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/mysql/permissions.yaml
================================================
permissions:
- alter
- alter_routine
- allow_nonexistent_definer
- application_password_admin
- audit_abort_exempt
- audit_admin
- authentication_policy_admin
- backup_admin
- binlog_admin
- binlog_encryption_admin
- clone_admin
- connection_admin
- create
- create_role
- create_routine
- create_tablespace
- create_temporary_tables
- create_user
- create_view
- delete
- drop
- drop_role
- encryption_key_admin
- event
- execute
- file
- firewall_admin
- firewall_exempt
- firewall_user
- flush_optimizer_costs
- flush_status
- flush_tables
- flush_user_resources
- grant_option
- group_replication_admin
- group_replication_stream
- index
- innodb_redo_log_archive
- innodb_redo_log_enable
- insert
- locking_tables
- masking_dictionaries_admin
- ndb_stored_user
- passwordless_user_admin
- persist_ro_variables_admin
- process
- proxy
- references
- reload
- replication_applier
- replication_client
- replication_slave
- replication_slave_admin
- resource_group_admin
- resource_group_user
- role_admin
- select
- sensitive_variables_observer
- service_connection_admin
- session_variables_admin
- set_any_definer
- set_user_id
- show_databases
- show_routine
- show_view
- shutdown
- skip_query_rewrite
- super
- system_user
- system_variables_admin
- table_encryption_admin
- telemetry_log_admin
- tp_connection_admin
- transaction_gtid_tag
- trigger
- update
- usage
- version_token_admin
- xa_recover_admin
================================================
FILE: pkg/analyzer/analyzers/mysql/scopes.go
================================================
package mysql
type PrivTypes struct {
Global bool
Database bool
Table bool
Column bool
Routine bool
Proxy bool
Dynamic bool
}
// https://dev.mysql.com/doc/refman/8.0/en/grant.html#grant-global-privileges:~:text=%27localhost%27%3B-,Privileges%20Supported%20by%20MySQL,-The%20following%20tables
var SCOPES = map[string]PrivTypes{
// Static privs
"ALTER": {Global: true, Database: true, Table: true},
"ALTER ROUTINE": {Global: true, Database: true, Routine: true},
"CREATE": {Global: true, Database: true, Table: true},
"CREATE ROLE": {Global: true},
"CREATE ROUTINE": {Global: true, Database: true},
"CREATE TABLESPACE": {Global: true},
"CREATE TEMPORARY TABLES": {Global: true, Database: true},
"CREATE USER": {Global: true},
"CREATE VIEW": {Global: true, Database: true, Table: true},
"DELETE": {Global: true, Database: true, Table: true},
"DROP": {Global: true, Database: true, Table: true},
"DROP ROLE": {Global: true},
"EVENT": {Global: true, Database: true},
"EXECUTE": {Global: true, Database: true, Routine: true},
"FILE": {Global: true},
"GRANT OPTION": {Global: true, Database: true, Table: true, Routine: true, Proxy: true}, // Not granted on ALL PRIVILEGES
"INDEX": {Global: true, Database: true, Table: true},
"INSERT": {Global: true, Database: true, Table: true, Column: true},
"LOCK TABLES": {Global: true, Database: true},
"PROCESS": {Global: true},
"PROXY": {Proxy: true}, // Not granted on ALL PRIVILEGES
"REFERENCES": {Global: true, Database: true, Table: true, Column: true},
"RELOAD": {Global: true},
"REPLICATION CLIENT": {Global: true},
"REPLICATION SLAVE": {Global: true},
"SELECT": {Global: true, Database: true, Table: true, Column: true},
"SHOW DATABASES": {Global: true},
"SHOW VIEW": {Global: true, Database: true, Table: true},
"SHUTDOWN": {Global: true},
"SUPER": {Global: true},
"TRIGGER": {Global: true, Database: true, Table: true},
"UPDATE": {Global: true, Database: true, Table: true, Column: true},
// This is a special case, it's not a real privilege
"USAGE": {Global: true, Database: true, Table: true, Column: true, Routine: true},
// Dynamic privs
"ALLOW_NONEXISTENT_DEFINER": {Global: true, Dynamic: true},
"APPLICATION_PASSWORD_ADMIN": {Global: true, Dynamic: true},
"AUDIT_ABORT_EXEMPT": {Global: true, Dynamic: true},
"AUDIT_ADMIN": {Global: true, Dynamic: true},
"AUTHENTICATION_POLICY_ADMIN": {Global: true, Dynamic: true},
"BACKUP_ADMIN": {Global: true, Dynamic: true},
"BINLOG_ADMIN": {Global: true, Dynamic: true},
"BINLOG_ENCRYPTION_ADMIN": {Global: true, Dynamic: true},
"CLONE_ADMIN": {Global: true, Dynamic: true},
"CONNECTION_ADMIN": {Global: true, Dynamic: true},
"ENCRYPTION_KEY_ADMIN": {Global: true, Dynamic: true},
"FIREWALL_ADMIN": {Global: true, Dynamic: true},
"FIREWALL_EXEMPT": {Global: true, Dynamic: true},
"FIREWALL_USER": {Global: true, Dynamic: true},
"FLUSH_OPTIMIZER_COSTS": {Global: true, Dynamic: true},
"FLUSH_STATUS": {Global: true, Dynamic: true},
"FLUSH_TABLES": {Global: true, Dynamic: true},
"FLUSH_USER_RESOURCES": {Global: true, Dynamic: true},
"GROUP_REPLICATION_ADMIN": {Global: true, Dynamic: true},
"GROUP_REPLICATION_STREAM": {Global: true, Dynamic: true},
"INNODB_REDO_LOG_ARCHIVE": {Global: true, Dynamic: true},
"INNODB_REDO_LOG_ENABLE": {Global: true, Dynamic: true},
"MASKING_DICTIONARIES_ADMIN": {Global: true, Dynamic: true},
"NDB_STORED_USER": {Global: true, Dynamic: true},
"PASSWORDLESS_USER_ADMIN": {Global: true, Dynamic: true},
"PERSIST_RO_VARIABLES_ADMIN": {Global: true, Dynamic: true},
"REPLICATION_APPLIER": {Global: true, Dynamic: true},
"REPLICATION_SLAVE_ADMIN": {Global: true, Dynamic: true},
"RESOURCE_GROUP_ADMIN": {Global: true, Dynamic: true},
"RESOURCE_GROUP_USER": {Global: true, Dynamic: true},
"ROLE_ADMIN": {Global: true, Dynamic: true},
"SENSITIVE_VARIABLES_OBSERVER": {Global: true, Dynamic: true},
"SERVICE_CONNECTION_ADMIN": {Global: true, Dynamic: true},
"SESSION_VARIABLES_ADMIN": {Global: true, Dynamic: true},
"SET_ANY_DEFINER": {Global: true, Dynamic: true},
"SET_USER_ID": {Global: true, Dynamic: true},
"SHOW_ROUTINE": {Global: true, Dynamic: true},
"SKIP_QUERY_REWRITE": {Global: true, Dynamic: true},
"SYSTEM_USER": {Global: true, Dynamic: true},
"SYSTEM_VARIABLES_ADMIN": {Global: true, Dynamic: true},
"TABLE_ENCRYPTION_ADMIN": {Global: true, Dynamic: true},
"TELEMETRY_LOG_ADMIN": {Global: true, Dynamic: true},
"TP_CONNECTION_ADMIN": {Global: true, Dynamic: true},
"TRANSACTION_GTID_TAG": {Global: true, Dynamic: true},
"VERSION_TOKEN_ADMIN": {Global: true, Dynamic: true},
"XA_RECOVER_ADMIN": {Global: true, Dynamic: true},
}
================================================
FILE: pkg/analyzer/analyzers/netlify/models.go
================================================
package netlify
import "sync"
type ResourceType string
func (r ResourceType) String() string {
return string(r)
}
const (
CurrentUser ResourceType = "User"
Token ResourceType = "Token"
Site ResourceType = "Site"
SiteFile ResourceType = "Site File"
SiteEnvVar ResourceType = "Site Env Variable"
SiteSnippet ResourceType = "Site Snippet"
SiteDeploy ResourceType = "Site Deploy"
SiteDeployedBranch ResourceType = "Site Deployed Branch"
SiteBuild ResourceType = "Site Build"
SiteDevServer ResourceType = "Site Dev Server"
SiteBuildHook ResourceType = "Site Build Hook"
SiteDevServerHook ResourceType = "Site Dev Server Hook"
SiteServiceInstance ResourceType = "Site Service Instance"
SiteFunction ResourceType = "Site Function"
SiteForm ResourceType = "Site Form"
SiteSubmission ResourceType = "Site Submission"
SiteTrafficSplit ResourceType = "Site Traffic Split"
DNSZone ResourceType = "DNS Zone"
Service ResourceType = "Service"
)
type SecretInfo struct {
mu sync.RWMutex
UserInfo User
Resources []NetlifyResource
}
func (s *SecretInfo) appendResource(resource NetlifyResource) {
s.mu.Lock()
defer s.mu.Unlock()
s.Resources = append(s.Resources, resource)
}
// listResourceByType returns a list of resources matching the given type.
func (s *SecretInfo) listResourceByType(resourceType ResourceType) []NetlifyResource {
s.mu.RLock()
defer s.mu.RUnlock()
resources := make([]NetlifyResource, 0, len(s.Resources))
for _, resource := range s.Resources {
if resource.Type == resourceType.String() {
resources = append(resources, resource)
}
}
return resources
}
type User struct {
ID string `json:"id"`
Name string `json:"full_name"`
Email string `json:"email"`
AccountID string `json:"account_id"`
LastLogin string `json:"last_login"`
}
type NetlifyResource struct {
ID string
Name string
Type string
Metadata map[string]string
Parent *NetlifyResource
}
type token struct {
ID string `json:"id"`
Name string `json:"name"`
Personal bool `json:"personal"`
ExpiresAt string `json:"expires_at"`
}
type site struct {
SiteID string `json:"site_id"`
Name string `json:"name"`
Url string `json:"url"`
AdminUrl string `json:"admin_url"`
RepoUrl string `json:"repo_url"`
}
type file struct {
ID string `json:"id"`
Path string `json:"path"`
MimeType string `json:"mime_type"`
}
type envVariable struct {
Key string `json:"key"`
Scopes []string `json:"scopes"`
Values []struct {
ID string `json:"id"`
Value string `json:"value"`
} `json:"values"`
}
type snippet struct {
ID string `json:"id"`
Title string `json:"title"`
}
type deploy struct {
ID string `json:"id"`
Name string `json:"name"`
BuildID string `json:"build_id"`
State string `json:"state"`
Url string `json:"url"`
}
type deployedBranch struct {
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
}
type build struct {
ID string `json:"id"`
DeployState string `json:"deploy_state"`
}
type devServer struct {
ID string `json:"id"`
Title string `json:"title"`
}
type buildHook struct {
ID string `json:"id"`
Title string `json:"title"`
Branch string `json:"branch"`
}
type serviceInstance struct {
ID string `json:"id"`
ServiceName string `json:"service_name"`
Url string `json:"url"`
}
type function struct {
ID string `json:"id"`
Provider string `json:"provider"`
}
// this handle response of 3 API's
type formSubmissionSplitInfo struct {
ID string `json:"id"`
Name string `json:"name"`
}
type dnsZone struct {
ID string `json:"id"`
Name string `json:"name"`
}
type service struct {
ID string `json:"id"`
Name string `json:"name"`
ServicePath string `json:"service_path"`
}
================================================
FILE: pkg/analyzer/analyzers/netlify/netlify.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go netlify
package netlify
import (
"fmt"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeNetlify
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, exist := credInfo["key"]
if !exist {
return nil, fmt.Errorf("key not found in credential info")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
// just print the error in cli and continue as a partial success
color.Red("[x] Error : %s", err.Error())
}
if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}
color.Green("[!] Valid Netlify API key\n\n")
printUserInfo(info.UserInfo)
printTokenInfo(info.listResourceByType(Token))
printResources(info.Resources)
color.Yellow("\n[i] Expires: %s", "N/A (Refer to Token Information Table)")
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
client := analyzers.NewAnalyzeClient(cfg)
var secretInfo = &SecretInfo{}
if err := captureUserInfo(client, key, secretInfo); err != nil {
return nil, err
}
if err := captureTokens(client, key, secretInfo); err != nil {
return nil, err
}
if err := captureResources(client, key, secretInfo); err != nil {
return secretInfo, err
}
return secretInfo, nil
}
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeNetlify,
Metadata: map[string]any{},
Bindings: make([]analyzers.Binding, 0),
}
// extract information from resource to create bindings and append to result bindings
for _, resource := range info.Resources {
binding := analyzers.Binding{
Resource: analyzers.Resource{
Name: resource.Name,
FullyQualifiedName: fmt.Sprintf("netlify/%s/%s", resource.Type, resource.ID), // e.g: netlify/site/123
Type: resource.Type,
Metadata: map[string]any{}, // to avoid panic
},
Permission: analyzers.Permission{
Value: PermissionStrings[FullAccess], // no fine grain access
},
}
if resource.Parent != nil {
binding.Resource.Parent = &analyzers.Resource{
Name: resource.Parent.Name,
FullyQualifiedName: resource.Parent.ID,
Type: resource.Parent.Type,
// not copying parent metadata
}
}
for key, value := range resource.Metadata {
binding.Resource.Metadata[key] = value
}
result.Bindings = append(result.Bindings, binding)
}
return &result
}
// cli print functions
func printUserInfo(user User) {
color.Yellow("[i] User Information:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Name", "Email", "Account ID", "Last Login At"})
t.AppendRow(table.Row{color.GreenString(user.Name), color.GreenString(user.Email), color.GreenString(user.AccountID), color.GreenString(user.LastLogin)})
t.Render()
}
func printTokenInfo(tokens []NetlifyResource) {
color.Yellow("[i] Tokens Information:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"ID", "Name", "Personal", "Expires At"})
for _, token := range tokens {
t.AppendRow(table.Row{color.GreenString(token.ID), color.GreenString(token.Name), color.GreenString(token.Metadata[tokenPersonal]), color.GreenString(token.Metadata[tokenExpiresAt])})
}
t.Render()
}
func printResources(resources []NetlifyResource) {
color.Yellow("[i] Resources:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Name", "Type"})
for _, resource := range resources {
// skip token type resource as we will print them separately
if resource.Type == Token.String() {
continue
}
t.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/netlify/netlify_test.go
================================================
package netlify
import (
_ "embed"
"encoding/json"
"fmt"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed result_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "analyzers1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
key := testSecrets.MustGetField("NETLIFY_PAT")
tests := []struct {
name string
key string
want []byte // JSON string
wantErr bool
}{
{
name: "valid netlify personal access token",
key: key,
want: expectedOutput,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
fmt.Println(string(gotJSON))
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/netlify/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package netlify
import "errors"
type Permission int
const (
Invalid Permission = iota
FullAccess Permission = iota
)
var (
PermissionStrings = map[Permission]string{
FullAccess: "full_access",
}
StringToPermission = map[string]Permission{
"full_access": FullAccess,
}
PermissionIDs = map[Permission]int{
FullAccess: 1,
}
IdToPermission = map[int]Permission{
1: FullAccess,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/netlify/permissions.yaml
================================================
permissions:
- full_access
================================================
FILE: pkg/analyzer/analyzers/netlify/requests.go
================================================
package netlify
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"sync"
)
var (
apiEndpoints = map[ResourceType]string{
CurrentUser: "https://api.netlify.com/api/v1/user",
Token: "https://app.netlify.com/access-control/bb-api/api/v1/oauth/applications", // undocumented API - return personal tokens with metadata
Site: "https://api.netlify.com/api/v1/sites",
SiteFile: "https://api.netlify.com/api/v1/sites/%s/files", // require site id
SiteEnvVar: "https://api.netlify.com/api/v1/sites/%s/env", // require site id
SiteSnippet: "https://api.netlify.com/api/v1/sites/%s/snippets", // require site id
SiteDeploy: "https://api.netlify.com/api/v1/sites/%s/deploys", // require site id
SiteDeployedBranch: "https://api.netlify.com/api/v1/sites/%s/deployed-branches", // require site id
SiteBuild: "https://api.netlify.com/api/v1/sites/%s/builds", // require site id
SiteDevServer: "https://api.netlify.com/api/v1/sites/%s/dev_servers", // require site id
SiteBuildHook: "https://api.netlify.com/api/v1/sites/%s/build_hooks", // require site id
SiteDevServerHook: "https://api.netlify.com/api/v1/sites/%s/dev_server_hooks", // require site id
SiteServiceInstance: "https://api.netlify.com/api/v1/sites/%s/service-instances", // require site id
SiteFunction: "https://api.netlify.com/api/v1/sites/%s/functions", // require site id
SiteForm: "https://api.netlify.com/api/v1/sites/%s/forms", // require site id
SiteSubmission: "https://api.netlify.com/api/v1/sites/%s/submissions", // require site id
SiteTrafficSplit: "https://api.netlify.com/api/v1/sites/%s/traffic_splits", // require site id
DNSZone: "https://api.netlify.com/api/v1/dns_zones",
Service: "https://api.netlify.com/api/v1/services",
/*
TODO APIs:
- https://api.netlify.com/api/v1/sites/{site_id}/metadata (Just return key and values added as metadata for a site)
- https://api.netlify.com/api/v1/sites/{site_id}/assets/{asset_id} (Require asset id - No API to list assets)
- https://api.netlify.com/api/v1/deploy_keys (Have id and a public key in response only)
*/
}
// metadata keys - should always start with resource name
tokenPersonal = "personal"
tokenExpiresAt = "expires_at"
siteUrl = "site_url"
siteAdminUrl = "site_admin_url"
siteRepoUrl = "site_repo_url"
fileMimeType = "site_mime_type"
deployBuildID = "deploy_build_id"
deployState = "deploy_state"
deployUrl = "deploy_url"
deployedBranchSlug = "deployed_branch_slug"
buildHookBranch = "build_hook_branch"
serviceInstanceUrl = "service_instance_url"
)
// makeNetlifyRequest send the API request to passed url with passed key as personal access token and return response body and status code
func makeNetlifyRequest(client *http.Client, endpoint, key string) ([]byte, int, error) {
// create request
req, err := http.NewRequest(http.MethodGet, endpoint, http.NoBody)
if err != nil {
return nil, 0, err
}
// add key in the header
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
resp, err := client.Do(req)
if err != nil {
return nil, 0, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
responseBodyByte, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, err
}
return responseBodyByte, resp.StatusCode, nil
}
// captureResources try to capture all the resource that the key can access
func captureResources(client *http.Client, key string, secretInfo *SecretInfo) error {
var (
wg sync.WaitGroup
errAggWg sync.WaitGroup
aggregatedErrs = make([]error, 0)
errChan = make(chan error, 1)
)
errAggWg.Add(1)
go func() {
defer errAggWg.Done()
for err := range errChan {
aggregatedErrs = append(aggregatedErrs, err)
}
}()
// helper to launch tasks concurrently.
launchTask := func(task func() error) {
wg.Add(1)
go func() {
defer wg.Done()
if err := task(); err != nil {
errChan <- err
}
}()
}
// capture top level resources
if err := captureSites(client, key, secretInfo); err != nil {
return err
}
// capture all sub resources of all sites
sites := secretInfo.listResourceByType(Site)
for _, site := range sites {
launchTask(func() error { return captureSiteFiles(client, key, site, secretInfo) })
launchTask(func() error { return captureSiteEnvVar(client, key, site, secretInfo) })
launchTask(func() error { return captureSiteSnippets(client, key, site, secretInfo) })
launchTask(func() error { return captureSiteDeploys(client, key, site, secretInfo) })
launchTask(func() error { return captureSiteDeployedBranches(client, key, site, secretInfo) })
launchTask(func() error { return captureSiteBuilds(client, key, site, secretInfo) })
launchTask(func() error { return captureSiteDevServers(client, key, site, secretInfo) })
launchTask(func() error { return captureSiteBuildHooks(client, key, site, secretInfo) })
launchTask(func() error { return captureSiteDevServerHooks(client, key, site, secretInfo) })
launchTask(func() error { return captureSiteServiceInstances(client, key, site, secretInfo) })
launchTask(func() error { return captureSiteFunctions(client, key, site, secretInfo) })
launchTask(func() error { return captureSiteFormSubmissionSplitInfo(client, key, site, SiteForm, secretInfo) })
launchTask(func() error { return captureSiteFormSubmissionSplitInfo(client, key, site, SiteSubmission, secretInfo) })
launchTask(func() error {
return captureSiteFormSubmissionSplitInfo(client, key, site, SiteTrafficSplit, secretInfo)
})
}
launchTask(func() error { return captureDNSZones(client, key, secretInfo) })
launchTask(func() error { return captureServices(client, key, secretInfo) })
wg.Wait()
close(errChan)
errAggWg.Wait()
if len(aggregatedErrs) > 0 {
return errors.Join(aggregatedErrs...)
}
return nil
}
func captureUserInfo(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, apiEndpoints[CurrentUser], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var user User
if err := json.Unmarshal(respBody, &user); err != nil {
return err
}
secretInfo.UserInfo = user
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d for API: %s", statusCode, apiEndpoints[CurrentUser])
}
}
func captureTokens(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, apiEndpoints[Token], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var tokens []token
if err := json.Unmarshal(respBody, &tokens); err != nil {
return err
}
for _, token := range tokens {
if token.ExpiresAt == "" {
token.ExpiresAt = "never"
}
resource := NetlifyResource{
ID: token.ID,
Name: token.Name,
Type: Token.String(),
Metadata: map[string]string{
tokenExpiresAt: token.ExpiresAt,
tokenPersonal: strconv.FormatBool(token.Personal),
},
}
secretInfo.Resources = append(secretInfo.Resources, resource)
}
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d for API: %s", statusCode, apiEndpoints[Token])
}
}
func captureSites(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, apiEndpoints[Site], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var sites []site
if err := json.Unmarshal(respBody, &sites); err != nil {
return err
}
for _, site := range sites {
secretInfo.appendResource(NetlifyResource{
ID: site.SiteID,
Name: site.Name,
Type: Site.String(),
Metadata: map[string]string{
siteUrl: site.Url,
siteAdminUrl: site.AdminUrl,
siteRepoUrl: site.RepoUrl,
},
})
}
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
func captureSiteFiles(client *http.Client, key string, site NetlifyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, fmt.Sprintf(apiEndpoints[SiteFile], site.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var files []file
if err := json.Unmarshal(respBody, &files); err != nil {
return err
}
for _, file := range files {
secretInfo.appendResource(NetlifyResource{
ID: site.ID + "/" + file.ID, // combine site id with file id to make it unique
Name: file.Path,
Type: SiteFile.String(),
Metadata: map[string]string{
fileMimeType: file.MimeType,
},
Parent: &site,
})
}
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
func captureSiteEnvVar(client *http.Client, key string, site NetlifyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, fmt.Sprintf(apiEndpoints[SiteEnvVar], site.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var envVariables []envVariable
if err := json.Unmarshal(respBody, &envVariables); err != nil {
return err
}
for _, envVar := range envVariables {
// multiple values exist for each env variable, so we append separate resource for each value
for _, value := range envVar.Values {
secretInfo.appendResource(NetlifyResource{
ID: envVar.Key + "/" + value.ID,
Name: envVar.Key + "/***" + value.Value[len(value.Value)-4:], // append last 4 characters of value with key to make it unique
Type: SiteEnvVar.String(),
Metadata: map[string]string{
"value": value.Value,
"scopes": strings.Join(envVar.Scopes, ";"),
},
Parent: &site,
})
}
}
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
func captureSiteSnippets(client *http.Client, key string, site NetlifyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, fmt.Sprintf(apiEndpoints[SiteSnippet], site.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var snippets []snippet
if err := json.Unmarshal(respBody, &snippets); err != nil {
return err
}
for _, snippet := range snippets {
secretInfo.appendResource(NetlifyResource{
ID: snippet.ID,
Name: snippet.Title,
Type: SiteSnippet.String(),
Parent: &site,
})
}
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
func captureSiteDeploys(client *http.Client, key string, site NetlifyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, fmt.Sprintf(apiEndpoints[SiteDeploy], site.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var deploys []deploy
if err := json.Unmarshal(respBody, &deploys); err != nil {
return err
}
for _, deploy := range deploys {
secretInfo.appendResource(NetlifyResource{
ID: site.ID + "/deploy/" + deploy.ID,
Name: deploy.Name,
Type: SiteDeploy.String(),
Metadata: map[string]string{
deployBuildID: deploy.BuildID,
deployState: deploy.State,
deployUrl: deploy.Url,
},
Parent: &site,
})
}
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
func captureSiteDeployedBranches(client *http.Client, key string, site NetlifyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, fmt.Sprintf(apiEndpoints[SiteDeployedBranch], site.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var deployedBranches []deployedBranch
if err := json.Unmarshal(respBody, &deployedBranches); err != nil {
return err
}
for _, deployedBranch := range deployedBranches {
secretInfo.appendResource(NetlifyResource{
ID: deployedBranch.ID,
Name: deployedBranch.Name,
Type: SiteDeployedBranch.String(),
Metadata: map[string]string{
deployedBranchSlug: deployedBranch.Slug,
},
Parent: &site,
})
}
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
func captureSiteBuilds(client *http.Client, key string, site NetlifyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, fmt.Sprintf(apiEndpoints[SiteBuild], site.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var builds []build
if err := json.Unmarshal(respBody, &builds); err != nil {
return err
}
for _, build := range builds {
secretInfo.appendResource(NetlifyResource{
ID: build.ID,
Name: build.ID + "/state/" + build.DeployState, // no specific name
Type: SiteBuild.String(),
Parent: &site,
})
}
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
func captureSiteDevServers(client *http.Client, key string, site NetlifyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, fmt.Sprintf(apiEndpoints[SiteDevServer], site.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var devServers []devServer
if err := json.Unmarshal(respBody, &devServers); err != nil {
return err
}
for _, devServer := range devServers {
secretInfo.appendResource(NetlifyResource{
ID: devServer.ID,
Name: devServer.Title,
Type: SiteDevServer.String(),
Parent: &site,
})
}
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
func captureSiteBuildHooks(client *http.Client, key string, site NetlifyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, fmt.Sprintf(apiEndpoints[SiteBuildHook], site.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var hooks []buildHook
if err := json.Unmarshal(respBody, &hooks); err != nil {
return err
}
for _, hook := range hooks {
secretInfo.appendResource(NetlifyResource{
ID: hook.ID,
Name: hook.Title,
Type: SiteBuildHook.String(),
Metadata: map[string]string{
buildHookBranch: hook.Branch,
},
Parent: &site,
})
}
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
func captureSiteDevServerHooks(client *http.Client, key string, site NetlifyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, fmt.Sprintf(apiEndpoints[SiteDevServerHook], site.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var devServerHooks []buildHook
if err := json.Unmarshal(respBody, &devServerHooks); err != nil {
return err
}
for _, hook := range devServerHooks {
secretInfo.appendResource(NetlifyResource{
ID: hook.ID,
Name: hook.Title,
Type: SiteDevServerHook.String(),
Metadata: map[string]string{
buildHookBranch: hook.Branch,
},
Parent: &site,
})
}
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
func captureSiteServiceInstances(client *http.Client, key string, site NetlifyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, fmt.Sprintf(apiEndpoints[SiteServiceInstance], site.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var serviceInstances []serviceInstance
if err := json.Unmarshal(respBody, &serviceInstances); err != nil {
return err
}
for _, instance := range serviceInstances {
secretInfo.appendResource(NetlifyResource{
ID: instance.ID,
Name: instance.ServiceName + "/instance/" + instance.ID, // no specific name
Type: SiteServiceInstance.String(),
Metadata: map[string]string{
serviceInstanceUrl: instance.Url,
},
Parent: &site,
})
}
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
func captureSiteFunctions(client *http.Client, key string, site NetlifyResource, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, fmt.Sprintf(apiEndpoints[SiteFunction], site.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var data function
if err := json.Unmarshal(respBody, &data); err != nil {
return err
}
secretInfo.appendResource(NetlifyResource{
ID: data.ID,
Name: "function/" + data.ID + "/provider/" + data.Provider, // no specific name
Type: SiteFunction.String(),
Parent: &site,
})
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
func captureSiteFormSubmissionSplitInfo(client *http.Client, key string, site NetlifyResource, resType ResourceType, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, fmt.Sprintf(apiEndpoints[resType], site.ID), key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var formSubSplitInfos []formSubmissionSplitInfo
if err := json.Unmarshal(respBody, &formSubSplitInfos); err != nil {
return err
}
for _, info := range formSubSplitInfos {
secretInfo.appendResource(NetlifyResource{
ID: info.ID,
Name: info.Name, // no specific name
Type: resType.String(),
Parent: &site,
})
}
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
func captureDNSZones(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, apiEndpoints[DNSZone], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var dnsZones []dnsZone
if err := json.Unmarshal(respBody, &dnsZones); err != nil {
return err
}
for _, dnsZone := range dnsZones {
secretInfo.appendResource(NetlifyResource{
ID: dnsZone.ID,
Name: dnsZone.Name,
Type: DNSZone.String(),
})
}
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
func captureServices(client *http.Client, key string, secretInfo *SecretInfo) error {
respBody, statusCode, err := makeNetlifyRequest(client, apiEndpoints[Service], key)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
var services []service
if err := json.Unmarshal(respBody, &services); err != nil {
return err
}
for _, service := range services {
secretInfo.appendResource(NetlifyResource{
ID: service.ID,
Name: service.Name,
Type: Service.String(),
})
}
return nil
case http.StatusUnauthorized:
return fmt.Errorf("invalid/expired personal access token")
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}
================================================
FILE: pkg/analyzer/analyzers/netlify/result_output.json
================================================
{
"AnalyzerType": 34,
"Bindings": [
{
"Resource": {
"Name": "/assets/404-bp-rpyh2.js",
"FullyQualifiedName": "netlify/Site File/dda81214-b126-43bf-9508-ae94cf9d0506//assets/404-bp-rpyh2.js",
"Type": "Site File",
"Metadata": {
"site_mime_type": "application/javascript"
},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "/assets/about-c6ru7nfs.js",
"FullyQualifiedName": "netlify/Site File/dda81214-b126-43bf-9508-ae94cf9d0506//assets/about-c6ru7nfs.js",
"Type": "Site File",
"Metadata": {
"site_mime_type": "application/javascript"
},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "/assets/index-bjt0jjds.css",
"FullyQualifiedName": "netlify/Site File/dda81214-b126-43bf-9508-ae94cf9d0506//assets/index-bjt0jjds.css",
"Type": "Site File",
"Metadata": {
"site_mime_type": "text/css"
},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "/assets/index-csbqlcvs.js",
"FullyQualifiedName": "netlify/Site File/dda81214-b126-43bf-9508-ae94cf9d0506//assets/index-csbqlcvs.js",
"Type": "Site File",
"Metadata": {
"site_mime_type": "application/javascript"
},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "/index.html",
"FullyQualifiedName": "netlify/Site File/dda81214-b126-43bf-9508-ae94cf9d0506//index.html",
"Type": "Site File",
"Metadata": {
"site_mime_type": "text/html"
},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "/netlify.toml",
"FullyQualifiedName": "netlify/Site File/dda81214-b126-43bf-9508-ae94cf9d0506//netlify.toml",
"Type": "Site File",
"Metadata": {
"site_mime_type": "application/octet-stream"
},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "680a1332fb8af883c4da6666/state/ready",
"FullyQualifiedName": "netlify/Site Build/680a1332fb8af883c4da6666",
"Type": "Site Build",
"Metadata": {},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Addon example",
"FullyQualifiedName": "netlify/Service/5ec5b30682bb8a00bad573ee",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "DataStax Astra",
"FullyQualifiedName": "netlify/Service/5fadc1941f0b1600909ffe94",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "DatoCMS Local",
"FullyQualifiedName": "netlify/Service/5bc77c2bac7ff24e6152b43c",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "DatoCMS Staging",
"FullyQualifiedName": "netlify/Service/5bc77adbac7ff24e6152b43b",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Demo add-on",
"FullyQualifiedName": "netlify/Service/5c1abf2cac7ff2374d58fce2",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "EXAMPLE_KEY/***ALUE",
"FullyQualifiedName": "netlify/Site Env Variable/EXAMPLE_KEY/0016840d-61c5-4e4a-aab4-8f9d73125846",
"Type": "Site Env Variable",
"Metadata": {
"scopes": "builds;functions;post_processing;runtime",
"value": "EXAMPLE_VALUE"
},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "EXAMPLE_KEY_2/***anch",
"FullyQualifiedName": "netlify/Site Env Variable/EXAMPLE_KEY_2/c5d9d7f5-52f9-48a8-a218-a80d294f347e",
"Type": "Site Env Variable",
"Metadata": {
"scopes": "builds;functions;runtime",
"value": "****************anch"
},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "EXAMPLE_KEY_2/***ocal",
"FullyQualifiedName": "netlify/Site Env Variable/EXAMPLE_KEY_2/b9df8f4a-7a18-4cc1-bf80-e0affa32569b",
"Type": "Site Env Variable",
"Metadata": {
"scopes": "builds;functions;runtime",
"value": "local"
},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "EXAMPLE_KEY_2/***od_1",
"FullyQualifiedName": "netlify/Site Env Variable/EXAMPLE_KEY_2/1973b53f-60ab-40ce-ac23-20f6f2241fb3",
"Type": "Site Env Variable",
"Metadata": {
"scopes": "builds;functions;runtime",
"value": "****************od_1"
},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "EXAMPLE_KEY_2/***ploy",
"FullyQualifiedName": "netlify/Site Env Variable/EXAMPLE_KEY_2/dab3426a-fb1e-405c-b89c-5143650e510e",
"Type": "Site Env Variable",
"Metadata": {
"scopes": "builds;functions;runtime",
"value": "****************ploy"
},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "EXAMPLE_KEY_2/***thon",
"FullyQualifiedName": "netlify/Site Env Variable/EXAMPLE_KEY_2/658308b6-0cef-4987-a2a7-3235997af270",
"Type": "Site Env Variable",
"Metadata": {
"scopes": "builds;functions;runtime",
"value": "****************thon"
},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "EXAMPLE_KEY_2/***view",
"FullyQualifiedName": "netlify/Site Env Variable/EXAMPLE_KEY_2/a210fa75-1194-42a2-8ff9-788bdf70d2b3",
"Type": "Site Env Variable",
"Metadata": {
"scopes": "builds;functions;runtime",
"value": "****************view"
},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Expired Token",
"FullyQualifiedName": "netlify/Token/680b33106d9ae981575b4dec",
"Type": "Token",
"Metadata": {
"expires_at": "2025-04-26T00:00:01.262Z",
"personal": "true"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Express Example",
"FullyQualifiedName": "netlify/Service/5b96e429ac7ff24ff6916ae1",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Fauna DB staging",
"FullyQualifiedName": "netlify/Service/5bbbea43ac7ff23902cc2a64",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "FaunaDB Cloud",
"FullyQualifiedName": "netlify/Service/5bcf902fac7ff255bfc36233",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Get off my lawn",
"FullyQualifiedName": "netlify/Service/5ce6f8be82bb8a00b9940dfd",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Hasura GraphQL Engine",
"FullyQualifiedName": "netlify/Service/5c196638ac7ff255c853647e",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Joy Staging",
"FullyQualifiedName": "netlify/Service/5d23c4e682bb8a00ba311f23",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Netlify CMS Media Manager",
"FullyQualifiedName": "netlify/Service/5b9addcdac7ff27e11b0d4e4",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Nimbella",
"FullyQualifiedName": "netlify/Service/5f6d15de1f0b1600903dde32",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Nimbella Staging",
"FullyQualifiedName": "netlify/Service/5d9e587082bb8a00bb94943f",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "No Exp Token",
"FullyQualifiedName": "netlify/Token/680b32c6bccfc08cd7732add",
"Type": "Token",
"Metadata": {
"expires_at": "never",
"personal": "true"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Takeshape CMS",
"FullyQualifiedName": "netlify/Service/5c9934a882bb8a00bc657db7",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Takeshape CMS staging",
"FullyQualifiedName": "netlify/Service/5c798b4582bb8a00b7504197",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "VGS Staging",
"FullyQualifiedName": "netlify/Service/5be1c5bfac7ff267e9ba987b",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Very Good Security",
"FullyQualifiedName": "netlify/Service/5c6f1bbf82bb8a00bcdea659",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "View source banner addon",
"FullyQualifiedName": "netlify/Service/5b9aef73ac7ff23d0a3fecd4",
"Type": "Service",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "analyzer-test(do not delete)",
"FullyQualifiedName": "netlify/Token/6810b09ab80020167d7525fe",
"Type": "Token",
"Metadata": {
"expires_at": "never",
"personal": "true"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "function//provider/",
"FullyQualifiedName": "netlify/Site Function/",
"Type": "Site Function",
"Metadata": {},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "hook1",
"FullyQualifiedName": "netlify/Site Build Hook/680a168ae30f218cd01bd4e8",
"Type": "Site Build Hook",
"Metadata": {
"build_hook_branch": "main"
},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "test-app",
"FullyQualifiedName": "netlify/Token/6809f1dbfb8af846a8da644f",
"Type": "Token",
"Metadata": {
"expires_at": "never",
"personal": "false"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "test01",
"FullyQualifiedName": "netlify/Token/6809f1b5830a5c43672123f8",
"Type": "Token",
"Metadata": {
"expires_at": "2025-05-24T08:09:25.796Z",
"personal": "true"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "truffle-test-site",
"FullyQualifiedName": "netlify/Site/dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": {
"site_admin_url": "https://app.netlify.com/sites/truffle-test-site",
"site_repo_url": "",
"site_url": "http://truffle-test-site.netlify.app"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "truffle-test-site",
"FullyQualifiedName": "netlify/Site Deploy/dda81214-b126-43bf-9508-ae94cf9d0506/deploy/680a1332fb8af883c4da6668",
"Type": "Site Deploy",
"Metadata": {
"deploy_build_id": "680a1332fb8af883c4da6666",
"deploy_state": "ready",
"deploy_url": "http://truffle-test-site.netlify.app"
},
"Parent": {
"Name": "truffle-test-site",
"FullyQualifiedName": "dda81214-b126-43bf-9508-ae94cf9d0506",
"Type": "Site",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "trufflesecurity.com",
"FullyQualifiedName": "netlify/DNS Zone/6809f163830a5c42ca212432",
"Type": "DNS Zone",
"Metadata": {},
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": {}
}
================================================
FILE: pkg/analyzer/analyzers/ngrok/expected_output.json
================================================
{"AnalyzerType":37,"Bindings":[{"Resource":{"Name":"ep_2wRn1EAlf7JqFe3RPJBRNW1IkTI","FullyQualifiedName":"endpoint/ep_2wRn1EAlf7JqFe3RPJBRNW1IkTI","Type":"endpoint","Metadata":{"bindings":["public"],"createdAt":"2025-04-30T11:37:16Z","host":"","hostport":"lightly-communal-lizard.ngrok-free.app:443","metadata":"","port":0,"proto":"https","publicURL":"https://lightly-communal-lizard.ngrok-free.app","region":"","type":"cloud","updatedAt":"2025-04-30T11:37:16Z","uri":"https://api.ngrok.com/endpoints/ep_2wRn1EAlf7JqFe3RPJBRNW1IkTI"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"rd_2wRmxH1k3oaR4HFnXkzac9uz9wr","FullyQualifiedName":"domain/rd_2wRmxH1k3oaR4HFnXkzac9uz9wr","Type":"domain","Metadata":{"createdAt":"2025-04-30T11:36:44Z","domain":"lightly-communal-lizard.ngrok-free.app","metadata":"","uri":"https://api.ngrok.com/reserved_domains/rd_2wRmxH1k3oaR4HFnXkzac9uz9wr"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"ak_2wRnGU2AMIxg7O737nC6geKeVlX","FullyQualifiedName":"api_key/ak_2wRnGU2AMIxg7O737nC6geKeVlX","Type":"api_key","Metadata":{"createdAt":"2025-04-30T11:39:17Z","description":"API Key for \"Truffle Detector\"","metadata":"","ownerID":"usr_2wRmmWE9P3ivK6dpF8sCaMd0X0V","uri":"https://api.ngrok.com/api_keys/ak_2wRnGU2AMIxg7O737nC6geKeVlX"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"ak_2wRnJq02PcmYrlH38sdE4rKZKZY","FullyQualifiedName":"api_key/ak_2wRnJq02PcmYrlH38sdE4rKZKZY","Type":"api_key","Metadata":{"createdAt":"2025-04-30T11:39:44Z","description":"API Key for \"Elliot\"","metadata":"","ownerID":"bot_2wRn8sBjuvH9ZmLbBzrKKJKFmHq","uri":"https://api.ngrok.com/api_keys/ak_2wRnJq02PcmYrlH38sdE4rKZKZY"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"cr_2wRnBCGRkEgPBKb5G6SLxr0h8tb","FullyQualifiedName":"authtoken/cr_2wRnBCGRkEgPBKb5G6SLxr0h8tb","Type":"authtoken","Metadata":{"acl":[],"createdAt":"2025-04-30T11:38:35Z","description":"Tunnel Authtoken for \"Elliot\"","metadata":"","ownerID":"bot_2wRn8sBjuvH9ZmLbBzrKKJKFmHq","uri":"https://api.ngrok.com/credentials/cr_2wRnBCGRkEgPBKb5G6SLxr0h8tb"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"cr_2wRmmYJ2BRKy3SFnusPWX5dRPVQ","FullyQualifiedName":"authtoken/cr_2wRmmYJ2BRKy3SFnusPWX5dRPVQ","Type":"authtoken","Metadata":{"acl":[],"createdAt":"2025-04-30T11:35:19Z","description":"credential for \"detectors@trufflesec.com\"","metadata":"","ownerID":"usr_2wRmmWE9P3ivK6dpF8sCaMd0X0V","uri":"https://api.ngrok.com/credentials/cr_2wRmmYJ2BRKy3SFnusPWX5dRPVQ"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"sshcr_2wRnP5xXhZ2uhXBPQcFOFqhynaa","FullyQualifiedName":"ssh_credential/sshcr_2wRnP5xXhZ2uhXBPQcFOFqhynaa","Type":"ssh_credential","Metadata":{"acl":[],"createdAt":"2025-04-30T11:40:26Z","description":"SSH Key for \"Truffle Detector\"","metadata":"","ownerID":"usr_2wRmmWE9P3ivK6dpF8sCaMd0X0V","publicKey":"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCo9S+bLqFTCzDA0TxJWaiPqddDnrHojOHCOnl+ZlRcbBrG9hM8IUmaJ+ZG63NIOkaqrlHGed7MK+SLqZIqi/TkuyHwu8kkBcPCayrHdgdb9NWLpRFaWN2A67Ww+/14rPEzY7KA5EDlmWow2IPK9Ayb+J5El6NRAhLS8AChupfmRjAOxciMUTdckTI2avr5R1sOddI8cutjfvuwQvFpJI1oJLbewUxZv8gOXuqbZScIx72NiZvtCDtktVjNVm6sib129P+vD3QzCwSuNGZIv9fUcQK7Y/rmMHyjDNfvaqm8HunINBV+kDxubfbIQBMCpj/HeuUVToQ3xyfqGaON0EPa","uri":"https://api.ngrok.com/ssh_credentials/sshcr_2wRnP5xXhZ2uhXBPQcFOFqhynaa"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"bot_2wRn8sBjuvH9ZmLbBzrKKJKFmHq","FullyQualifiedName":"bot_user/bot_2wRn8sBjuvH9ZmLbBzrKKJKFmHq","Type":"bot_user","Metadata":{"active":true,"createdAt":"2025-04-30T11:38:17Z","name":"Elliot","uri":"https://api.ngrok.com/bot_users/bot_2wRn8sBjuvH9ZmLbBzrKKJKFmHq"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"usr_2wRmmWE9P3ivK6dpF8sCaMd0X0V","FullyQualifiedName":"user/usr_2wRmmWE9P3ivK6dpF8sCaMd0X0V","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"full_access","Parent":null}}],"UnboundedResources":[{"Name":"Account Plan","FullyQualifiedName":"account_plan/Free","Type":"account_plan","Metadata":null,"Parent":null}],"Metadata":null}
================================================
FILE: pkg/analyzer/analyzers/ngrok/models.go
================================================
package ngrok
type apiKey struct {
ID string `json:"id"`
URI string `json:"uri"`
Description string `json:"description"`
Metadata string `json:"metadata"`
OwnerID string `json:"owner_id"`
CreatedAt string `json:"created_at"`
}
type authtoken struct {
ID string `json:"id"`
URI string `json:"uri"`
Description string `json:"description"`
Metadata string `json:"metadata"`
ACL []string `json:"acl"`
OwnerID string `json:"owner_id"`
CreatedAt string `json:"created_at"`
}
type sshCredential struct {
ID string `json:"id"`
URI string `json:"uri"`
Description string `json:"description"`
PublicKey string `json:"public_key"`
Metadata string `json:"metadata"`
ACL []string `json:"acl"`
OwnerID string `json:"owner_id"`
CreatedAt string `json:"created_at"`
}
type domain struct {
ID string `json:"id"`
URI string `json:"uri"`
Domain string `json:"domain"`
Metadata string `json:"metadata"`
CreatedAt string `json:"created_at"`
}
type endpoint struct {
ID string `json:"id"`
Region string `json:"region"`
Host string `json:"host"`
Port int64 `json:"port"`
PublicURL string `json:"public_url"`
Proto string `json:"proto"`
Hostport string `json:"hostport"`
Type string `json:"type"`
Bindings []string `json:"bindings"`
URI string `json:"uri"`
Metadata string `json:"metadata"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type botUser struct {
ID string `json:"id"`
URI string `json:"uri"`
Name string `json:"name"`
Active bool `json:"active"`
CreatedAt string `json:"created_at"`
}
type user struct {
ID string `json:"id"`
}
type paginatedResponse struct {
NextPageURI string `json:"next_page_uri"`
APIKeys []apiKey `json:"keys,omitempty"`
Authtokens []authtoken `json:"credentials,omitempty"`
SSHCredentials []sshCredential `json:"ssh_credentials,omitempty"`
Domains []domain `json:"reserved_domains,omitempty"`
Endpoints []endpoint `json:"endpoints,omitempty"`
BotUsers []botUser `json:"bot_users,omitempty"`
}
type secretInfo struct {
Users []user
BotUsers []botUser
APIKeys []apiKey
Authtokens []authtoken
SSHCredentials []sshCredential
Domains []domain
Endpoints []endpoint
AccountType AccountType
}
================================================
FILE: pkg/analyzer/analyzers/ngrok/ngrok.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go ngrok
package ngrok
import (
"errors"
"fmt"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
_ "embed"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
type AccountType string
const (
AccountFree AccountType = "Free"
AccountPaid AccountType = "Paid"
)
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeNgrok
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, exist := credInfo["key"]
if !exist {
return nil, errors.New("key not found in credentials info")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Invalid Ngrok Key\n")
color.Red("[x] Error : %s", err.Error())
return
}
if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}
color.Green("[i] Valid Ngrok API Key\n")
printAccountAndPermissions(info)
}
func AnalyzePermissions(cfg *config.Config, key string) (*secretInfo, error) {
// Ngrok API keys provide full access to all resources depending on the account type
// Free accounts have access to a limited set of resources.
client := analyzers.NewAnalyzeClient(cfg)
secretInfo := &secretInfo{}
if err := determineAccountType(client, secretInfo, key); err != nil {
return nil, err
}
if err := populateAllResources(client, secretInfo, key); err != nil {
return nil, err
}
return secretInfo, nil
}
func secretInfoToAnalyzerResult(info *secretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
bindings := []analyzers.Binding{}
fullAccessPermission := analyzers.Permission{
Value: PermissionStrings[FullAccess],
}
for _, endpoint := range info.Endpoints {
bindings = append(bindings, analyzers.Binding{
Resource: createEndpointResource(endpoint),
Permission: fullAccessPermission,
})
}
for _, domain := range info.Domains {
bindings = append(bindings, analyzers.Binding{
Resource: createDomainResource(domain),
Permission: fullAccessPermission,
})
}
for _, apiKey := range info.APIKeys {
bindings = append(bindings, analyzers.Binding{
Resource: createAPIKeyResource(apiKey),
Permission: fullAccessPermission,
})
}
for _, authtoken := range info.Authtokens {
bindings = append(bindings, analyzers.Binding{
Resource: createAuthtokenResource(authtoken),
Permission: fullAccessPermission,
})
}
for _, sshCredential := range info.SSHCredentials {
bindings = append(bindings, analyzers.Binding{
Resource: createSSHKeyResource(sshCredential),
Permission: fullAccessPermission,
})
}
for _, botUser := range info.BotUsers {
bindings = append(bindings, analyzers.Binding{
Resource: createBotUserResource(botUser),
Permission: fullAccessPermission,
})
}
for _, user := range info.Users {
bindings = append(bindings, analyzers.Binding{
Resource: createUserResource(user),
Permission: fullAccessPermission,
})
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeNgrok,
Metadata: nil,
Bindings: bindings,
UnboundedResources: []analyzers.Resource{{
Name: "Account Plan",
FullyQualifiedName: "account_plan/" + string(info.AccountType),
Type: "account_plan",
}},
}
return &result
}
func printAccountAndPermissions(info *secretInfo) {
accountIsFree := info.AccountType != AccountPaid
color.Yellow("[i] Account Type: %s", info.AccountType)
color.Yellow("\n[i] Permissions:")
t1 := table.NewWriter()
t1.AppendHeader(table.Row{"Resource", "Access Level"})
// Printing the access level to Ngrok resources
for _, resource := range ngrokResources {
accessLevel := "Full Access"
if resource.IsPaidFeature && accountIsFree {
accessLevel = "None"
}
t1.AppendRow(table.Row{
color.GreenString(resource.Name),
color.GreenString(accessLevel),
})
t1.AppendSeparator()
}
t1.SetOutputMirror(os.Stdout)
t1.Render()
color.Yellow("\n[i] Resources:")
t2 := table.NewWriter()
t2.SetTitle("User IDs")
t2.AppendHeader(table.Row{"ID"})
for _, user := range info.Users {
t2.AppendRow(table.Row{
color.GreenString(user.ID),
})
}
t2.SetOutputMirror(os.Stdout)
t2.Render()
t3 := table.NewWriter()
t3.SetTitle("Endpoints")
t3.AppendHeader(table.Row{"ID", "Region", "Public URL", "Type", "Created At", "Updated At"})
for _, endpoint := range info.Endpoints {
t3.AppendRow(table.Row{
color.GreenString(endpoint.ID),
color.GreenString(endpoint.Region),
color.GreenString(endpoint.PublicURL),
color.GreenString(endpoint.Type),
color.GreenString(endpoint.CreatedAt),
color.GreenString(endpoint.UpdatedAt),
})
}
t3.SetOutputMirror(os.Stdout)
t3.Render()
t4 := table.NewWriter()
t4.SetTitle("Domains")
t4.AppendHeader(table.Row{"ID", "Domain", "URI", "Created At"})
for _, domain := range info.Domains {
t4.AppendRow(table.Row{
color.GreenString(domain.ID),
color.GreenString(domain.Domain),
color.GreenString(domain.URI),
color.GreenString(domain.CreatedAt),
})
}
t4.SetOutputMirror(os.Stdout)
t4.Render()
t5 := table.NewWriter()
t5.SetTitle("API Keys")
t5.AppendHeader(table.Row{"ID", "Description", "Owner ID", "Created At"})
for _, key := range info.APIKeys {
t5.AppendRow(table.Row{
color.GreenString(key.ID),
color.GreenString(key.Description),
color.GreenString(key.OwnerID),
color.GreenString(key.CreatedAt),
})
}
t5.SetOutputMirror(os.Stdout)
t5.Render()
t6 := table.NewWriter()
t6.SetTitle("Authtokens")
t6.AppendHeader(table.Row{"ID", "Description", "Owner ID", "Created At"})
for _, token := range info.Authtokens {
t6.AppendRow(table.Row{
color.GreenString(token.ID),
color.GreenString(token.Description),
color.GreenString(token.OwnerID),
color.GreenString(token.CreatedAt),
})
}
t6.SetOutputMirror(os.Stdout)
t6.Render()
t7 := table.NewWriter()
t7.SetTitle("SSH Credentials")
t7.AppendHeader(table.Row{"ID", "Description", "Owner ID", "Created At"})
for _, key := range info.SSHCredentials {
t7.AppendRow(table.Row{
color.GreenString(key.ID),
color.GreenString(key.Description),
color.GreenString(key.OwnerID),
color.GreenString(key.CreatedAt),
})
}
t7.SetOutputMirror(os.Stdout)
t7.Render()
t8 := table.NewWriter()
t8.SetTitle("Bot Users")
t8.AppendHeader(table.Row{"ID", "Name", "Is Active", "Created At"})
for _, endpoint := range info.BotUsers {
isActive := "No"
if endpoint.Active {
isActive = "Yes"
}
t8.AppendRow(table.Row{
color.GreenString(endpoint.ID),
color.GreenString(endpoint.Name),
color.GreenString(isActive),
color.GreenString(endpoint.CreatedAt),
})
}
t8.SetOutputMirror(os.Stdout)
t8.Render()
fmt.Printf("%s: https://www.ngrok.com/developers/documentation\n\n", color.GreenString("Ref"))
}
================================================
FILE: pkg/analyzer/analyzers/ngrok/ngrok_test.go
================================================
package ngrok
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "analyzers1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
key := testSecrets.MustGetField("NGROK")
tests := []struct {
name string
secret string
want string
wantErr bool
}{
{
name: "valid ngrok credentials",
secret: key,
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{
"key": tt.secret,
})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Type == bindings[j].Resource.Type {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Type < bindings[j].Resource.Type
})
}
================================================
FILE: pkg/analyzer/analyzers/ngrok/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package ngrok
import "errors"
type Permission int
const (
Invalid Permission = iota
FullAccess Permission = iota
)
var (
PermissionStrings = map[Permission]string{
FullAccess: "full_access",
}
StringToPermission = map[string]Permission{
"full_access": FullAccess,
}
PermissionIDs = map[Permission]int{
FullAccess: 1,
}
IdToPermission = map[int]Permission{
1: FullAccess,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/ngrok/permissions.yaml
================================================
permissions:
- full_access
================================================
FILE: pkg/analyzer/analyzers/ngrok/requests.go
================================================
package ngrok
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
)
const (
ngrokAPIBaseURL = "https://api.ngrok.com"
reservedAddressesEndpoint = "/reserved_addrs"
domainsEndpoint = "/reserved_domains"
endpointsEndpoint = "/endpoints"
apiKeysEndpoint = "/api_keys"
sshCredentialsEndpoint = "/ssh_credentials"
authtokensEndpoint = "/credentials"
botUsersEndpoint = "/bot_users"
)
func determineAccountType(client *http.Client, info *secretInfo, key string) error {
// To determine if the account is free or paid, we can attempt to create a reserved address
// Reserved Addresses are only available to paid accounts, so if the response contains the
// error "ERR_NGROK_501", we can assume the account is on a free plan.
// Ref: https://ngrok.com/docs/errors/err_ngrok_501
const errorCodeFreeAccount = "ERR_NGROK_501"
url := fmt.Sprintf("%s%s", ngrokAPIBaseURL, reservedAddressesEndpoint)
body, statusCode, err := makeAPIRequest(client, http.MethodPost, url, key)
if err != nil {
return err
}
// The response should be a 400 Bad Request based on our request. Any other status code indicates an error.
if statusCode != http.StatusBadRequest {
return fmt.Errorf("unexpected status code: %d while determining account type", statusCode)
}
switch statusCode {
case http.StatusBadRequest:
if strings.Contains(string(body), errorCodeFreeAccount) {
info.AccountType = AccountFree
} else {
info.AccountType = AccountPaid
}
case http.StatusForbidden:
return fmt.Errorf("invalid API key or access forbidden: %s", body)
default:
return fmt.Errorf("unexpected status code: %d while determining account type", statusCode)
}
return nil
}
func populateAllResources(client *http.Client, info *secretInfo, key string) error {
// Fetch all resources and populate the secretInfo struct with the data
// This is a placeholder function. The actual implementation will depend on the API endpoints and response formats.
// For example, you might want to call different endpoints to fetch API keys, SSH keys, etc.
// Example of populating API keys
if err := populateEndpoints(client, info, key); err != nil {
return err
}
if err := populateDomains(client, info, key); err != nil {
return err
}
if err := populateAPIKeys(client, info, key); err != nil {
return err
}
if err := populateAuthtokens(client, info, key); err != nil {
return err
}
if err := populateSSHCredentials(client, info, key); err != nil {
return err
}
if err := populateBotUsers(client, info, key); err != nil {
return err
}
populateUsers(info)
return nil
}
func populateEndpoints(client *http.Client, info *secretInfo, key string) error {
url := fmt.Sprintf("%s%s", ngrokAPIBaseURL, endpointsEndpoint)
info.Endpoints = []endpoint{}
for {
res, err := fetchResources(client, url, key)
if err != nil {
return err
}
info.Endpoints = append(info.Endpoints, res.Endpoints...)
url = res.NextPageURI
if url == "" {
break
}
}
return nil
}
func populateAPIKeys(client *http.Client, info *secretInfo, key string) error {
url := fmt.Sprintf("%s%s", ngrokAPIBaseURL, apiKeysEndpoint)
info.APIKeys = []apiKey{}
for {
res, err := fetchResources(client, url, key)
if err != nil {
return err
}
info.APIKeys = append(info.APIKeys, res.APIKeys...)
url = res.NextPageURI
if url == "" {
break
}
}
return nil
}
func populateSSHCredentials(client *http.Client, info *secretInfo, key string) error {
url := fmt.Sprintf("%s%s", ngrokAPIBaseURL, sshCredentialsEndpoint)
info.SSHCredentials = []sshCredential{}
for {
res, err := fetchResources(client, url, key)
if err != nil {
return err
}
info.SSHCredentials = append(info.SSHCredentials, res.SSHCredentials...)
url = res.NextPageURI
if url == "" {
break
}
}
return nil
}
func populateAuthtokens(client *http.Client, info *secretInfo, key string) error {
url := fmt.Sprintf("%s%s", ngrokAPIBaseURL, authtokensEndpoint)
info.Authtokens = []authtoken{}
for {
res, err := fetchResources(client, url, key)
if err != nil {
return err
}
info.Authtokens = append(info.Authtokens, res.Authtokens...)
url = res.NextPageURI
if url == "" {
break
}
}
return nil
}
func populateDomains(client *http.Client, info *secretInfo, key string) error {
url := fmt.Sprintf("%s%s", ngrokAPIBaseURL, domainsEndpoint)
info.Domains = []domain{}
for {
res, err := fetchResources(client, url, key)
if err != nil {
return err
}
info.Domains = append(info.Domains, res.Domains...)
url = res.NextPageURI
if url == "" {
break
}
}
return nil
}
func populateBotUsers(client *http.Client, info *secretInfo, key string) error {
url := fmt.Sprintf("%s%s", ngrokAPIBaseURL, botUsersEndpoint)
info.BotUsers = []botUser{}
for {
res, err := fetchResources(client, url, key)
if err != nil {
return err
}
info.BotUsers = append(info.BotUsers, res.BotUsers...)
url = res.NextPageURI
if url == "" {
break
}
}
return nil
}
func fetchResources(client *http.Client, url string, key string) (*paginatedResponse, error) {
for {
body, status, err := makeAPIRequest(client, http.MethodGet, url, key)
if err != nil {
return nil, err
}
switch status {
case http.StatusOK:
var resource paginatedResponse
if err := json.Unmarshal(body, &resource); err != nil {
return nil, err
}
return &resource, nil
case http.StatusForbidden:
return nil, fmt.Errorf("invalid API key or access forbidden: %s", body)
default:
return nil, fmt.Errorf("unexpected status code: %d", status)
}
}
}
func populateUsers(info *secretInfo) {
// Creating a map to track unique user IDs to help in avoiding
// duplicates when adding users to the info.Users slice
uniqueUserIDs := map[string]bool{}
processOwnerID := func(ownerID string) {
if strings.HasPrefix(ownerID, "usr_") {
if uniqueUserIDs[ownerID] {
return
}
uniqueUserIDs[ownerID] = true
info.Users = append(info.Users, user{ID: ownerID})
}
}
for _, token := range info.Authtokens {
processOwnerID(token.OwnerID)
}
for _, sshKey := range info.SSHCredentials {
processOwnerID(sshKey.OwnerID)
}
for _, apiKey := range info.APIKeys {
processOwnerID(apiKey.OwnerID)
}
}
func makeAPIRequest(client *http.Client, method string, url string, key string) ([]byte, int, error) {
var reqBody io.Reader = nil
if method == http.MethodPost {
reqBody = strings.NewReader("{}")
}
req, err := http.NewRequest(method, url, reqBody)
if err != nil {
return nil, 0, err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Ngrok-Version", "2")
res, err := client.Do(req)
if err != nil {
return nil, 0, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return nil, 0, fmt.Errorf("failed to read response body: %w", err)
}
return bodyBytes, res.StatusCode, nil
}
// Functions to create analyzers.Resource objects for different resource types
func createEndpointResource(endpoint endpoint) analyzers.Resource {
return analyzers.Resource{
Name: endpoint.ID,
FullyQualifiedName: "endpoint/" + endpoint.ID,
Type: "endpoint",
Metadata: map[string]any{
"region": endpoint.Region,
"host": endpoint.Host,
"port": endpoint.Port,
"publicURL": endpoint.PublicURL,
"proto": endpoint.Proto,
"hostport": endpoint.Hostport,
"type": endpoint.Type,
"uri": endpoint.URI,
"bindings": endpoint.Bindings,
"metadata": endpoint.Metadata,
"createdAt": endpoint.CreatedAt,
"updatedAt": endpoint.UpdatedAt,
},
}
}
func createDomainResource(domain domain) analyzers.Resource {
return analyzers.Resource{
Name: domain.ID,
FullyQualifiedName: "domain/" + domain.ID,
Type: "domain",
Metadata: map[string]any{
"uri": domain.URI,
"domain": domain.Domain,
"metadata": domain.Metadata,
"createdAt": domain.CreatedAt,
},
}
}
func createAPIKeyResource(apiKey apiKey) analyzers.Resource {
return analyzers.Resource{
Name: apiKey.ID,
FullyQualifiedName: "api_key/" + apiKey.ID,
Type: "api_key",
Metadata: map[string]any{
"uri": apiKey.URI,
"description": apiKey.Description,
"metadata": apiKey.Metadata,
"ownerID": apiKey.OwnerID,
"createdAt": apiKey.CreatedAt,
},
}
}
func createSSHKeyResource(sshCredential sshCredential) analyzers.Resource {
return analyzers.Resource{
Name: sshCredential.ID,
FullyQualifiedName: "ssh_credential/" + sshCredential.ID,
Type: "ssh_credential",
Metadata: map[string]any{
"uri": sshCredential.URI,
"description": sshCredential.Description,
"publicKey": sshCredential.PublicKey,
"metadata": sshCredential.Metadata,
"acl": sshCredential.ACL,
"ownerID": sshCredential.OwnerID,
"createdAt": sshCredential.CreatedAt,
},
}
}
func createAuthtokenResource(authtoken authtoken) analyzers.Resource {
return analyzers.Resource{
Name: authtoken.ID,
FullyQualifiedName: "authtoken/" + authtoken.ID,
Type: "authtoken",
Metadata: map[string]any{
"uri": authtoken.URI,
"description": authtoken.Description,
"metadata": authtoken.Metadata,
"acl": authtoken.ACL,
"ownerID": authtoken.OwnerID,
"createdAt": authtoken.CreatedAt,
},
}
}
func createBotUserResource(botUser botUser) analyzers.Resource {
return analyzers.Resource{
Name: botUser.ID,
FullyQualifiedName: "bot_user/" + botUser.ID,
Type: "bot_user",
Metadata: map[string]any{
"uri": botUser.URI,
"name": botUser.Name,
"active": botUser.Active,
"createdAt": botUser.CreatedAt,
},
}
}
func createUserResource(user user) analyzers.Resource {
return analyzers.Resource{
Name: user.ID,
FullyQualifiedName: "user/" + user.ID,
Type: "user",
}
}
================================================
FILE: pkg/analyzer/analyzers/ngrok/resources.go
================================================
package ngrok
type ngrokResource struct {
Name string
IsPaidFeature bool
}
var ngrokResources = []ngrokResource{
{
Name: "Endpoints",
IsPaidFeature: false,
},
{
Name: "Domains",
IsPaidFeature: false,
},
{
Name: "Reserved Addresses",
IsPaidFeature: true,
},
{
Name: "TLS Certificates",
IsPaidFeature: true,
},
{
Name: "Kubernetes Operators",
IsPaidFeature: true,
},
{
Name: "Certificate Authorities",
IsPaidFeature: true,
},
{
Name: "IP Policies",
IsPaidFeature: true,
},
{
Name: "Policy Rules",
IsPaidFeature: true,
},
{
Name: "Application Users",
IsPaidFeature: true,
},
{
Name: "Application Sessions",
IsPaidFeature: true,
},
{
Name: "Agent Ingress",
IsPaidFeature: true,
},
{
Name: "Tunnels",
IsPaidFeature: false,
},
{
Name: "Tunnel Sessions",
IsPaidFeature: false,
},
{
Name: "Event Destinations",
IsPaidFeature: false,
},
{
Name: "Event Sources",
IsPaidFeature: false,
},
{
Name: "Event Subscriptions",
IsPaidFeature: false,
},
{
Name: "IP Restrictions",
IsPaidFeature: true,
},
{
Name: "API Keys",
IsPaidFeature: false,
},
{
Name: "SSH Credentials",
IsPaidFeature: false,
},
{
Name: "Authtokens",
IsPaidFeature: false,
},
{
Name: "Bot Users",
IsPaidFeature: false,
},
{
Name: "SSH Certificate Authorities",
IsPaidFeature: true,
},
{
Name: "SSH Host Certificates",
IsPaidFeature: true,
},
{
Name: "SSH User Certificates",
IsPaidFeature: true,
},
}
================================================
FILE: pkg/analyzer/analyzers/notion/expected_output.json
================================================
{"AnalyzerType":22,"Bindings":[{"Resource":{"Name":"hooman","FullyQualifiedName":"notion.so/bot/62faeec3-a948-4dd4-90ae-426e0b192902","Type":"bot","Metadata":{"workspace":"hoomanit"},"Parent":null},"Permission":{"Value":"insert_content","Parent":null}},{"Resource":{"Name":"hooman","FullyQualifiedName":"notion.so/bot/62faeec3-a948-4dd4-90ae-426e0b192902","Type":"bot","Metadata":{"workspace":"hoomanit"},"Parent":null},"Permission":{"Value":"read_content","Parent":null}},{"Resource":{"Name":"hooman","FullyQualifiedName":"notion.so/bot/62faeec3-a948-4dd4-90ae-426e0b192902","Type":"bot","Metadata":{"workspace":"hoomanit"},"Parent":null},"Permission":{"Value":"read_users_with_email","Parent":null}},{"Resource":{"Name":"hooman","FullyQualifiedName":"notion.so/bot/62faeec3-a948-4dd4-90ae-426e0b192902","Type":"bot","Metadata":{"workspace":"hoomanit"},"Parent":null},"Permission":{"Value":"update_content","Parent":null}}],"UnboundedResources":[{"Name":"hooman","FullyQualifiedName":"notion.so/person/3d0600fa-fa18-427d-8abc-58b662f0d209","Type":"person","Metadata":{"email":"rendyplayground@gmail.com"},"Parent":null}],"Metadata":null}
================================================
FILE: pkg/analyzer/analyzers/notion/notion.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go notion
package notion
import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeNotion }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, errors.New("missing key in credInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeNotion,
Metadata: nil,
Bindings: make([]analyzers.Binding, len(info.Permissions)),
UnboundedResources: make([]analyzers.Resource, 0, len(info.WorkspaceUsers)),
}
resource := analyzers.Resource{
Name: info.Bot.Name,
FullyQualifiedName: "notion.so/bot/" + info.Bot.Id,
Type: info.Bot.Type,
Metadata: map[string]interface{}{
"workspace": info.Bot.GetWorkspaceName(),
},
}
for idx, permission := range info.Permissions {
result.Bindings[idx] = analyzers.Binding{
Resource: resource,
Permission: analyzers.Permission{
Value: permission,
},
}
}
// We can find list of users in the current workspace
// if the API key has read_user permission, so these can be
// unbounded resources
for _, user := range info.WorkspaceUsers {
if info.Bot.Id == user.Id {
// Skip the bot itself
continue
}
unboundresource := analyzers.Resource{
Name: user.Name,
FullyQualifiedName: fmt.Sprintf("notion.so/%s/%s", user.Type, user.Id),
Type: user.Type, // person or bot
}
if user.Person.Email != "" {
unboundresource.Metadata = map[string]interface{}{
"email": user.Person.Email,
}
}
result.UnboundedResources = append(result.UnboundedResources, unboundresource)
}
return &result
}
//go:embed scopes.json
var scopesConfig []byte
type HttpStatusTest struct {
Endpoint string `json:"endpoint"`
Method string `json:"method"`
Payload interface{} `json:"payload"`
ValidStatuses []int `json:"valid_status_code"`
InvalidStatuses []int `json:"invalid_status_code"`
}
func StatusContains(status int, vals []int) bool {
for _, v := range vals {
if status == v {
return true
}
}
return false
}
func (h *HttpStatusTest) RunTest(cfg *config.Config, headers map[string]string) (bool, error) {
// If body data, marshal to JSON
var data io.Reader
if h.Payload != nil {
jsonData, err := json.Marshal(h.Payload)
if err != nil {
return false, err
}
data = bytes.NewBuffer(jsonData)
}
// Create new HTTP request
client := analyzers.NewAnalyzeClientUnrestricted(cfg)
req, err := http.NewRequest(h.Method, h.Endpoint, data)
if err != nil {
return false, err
}
// Add custom headers if provided
for key, value := range headers {
req.Header.Set(key, value)
}
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// Check response status code
switch {
case StatusContains(resp.StatusCode, h.ValidStatuses):
return true, nil
case StatusContains(resp.StatusCode, h.InvalidStatuses):
return false, nil
default:
return false, errors.New("error checking response status code")
}
}
type Scope struct {
Name string `json:"name"`
HttpTest HttpStatusTest `json:"test"`
}
func readInScopes() ([]Scope, error) {
var scopes []Scope
if err := json.Unmarshal(scopesConfig, &scopes); err != nil {
return nil, err
}
return scopes, nil
}
func getPermissions(cfg *config.Config, key string) ([]string, error) {
scopes, err := readInScopes()
if err != nil {
return nil, fmt.Errorf("reading in scopes: %w", err)
}
permissions := make([]string, 0, len(scopes))
for _, scope := range scopes {
status, err := scope.HttpTest.RunTest(cfg, map[string]string{"Authorization": "Bearer " + key, "Notion-Version": "2022-06-28"})
if err != nil {
return nil, fmt.Errorf("running test: %w", err)
}
if status {
permissions = append(permissions, scope.Name)
}
}
return permissions, nil
}
type SecretInfo struct {
Bot *bot
WorkspaceUsers []user
Permissions []string
}
type user struct {
Id string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Person struct {
Email string `json:"email"`
}
}
type bot struct {
Id string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Bot struct {
Owner *struct {
Type string `json:"type"`
}
WorkspaceName string `json:"workspace_name"`
} `json:"bot"`
}
func (b *bot) GetWorkspaceName() string {
return b.Bot.WorkspaceName
}
func (b *bot) OwnedBy() string {
if b.Bot.Owner != nil {
return b.Bot.Owner.Type
}
return "N/A"
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error : %s", err.Error())
return
}
color.Green("[!] Valid Notion API key\n\n")
color.Green("[i] Bot: %s (%s)\n", info.Bot.Name, info.Bot.Id)
color.Green("[i] Bot Owned By: %s\n", info.Bot.OwnedBy())
if info.Bot.GetWorkspaceName() != "" {
color.Green("[i] Workspace: %s\n\n", info.Bot.GetWorkspaceName())
}
printPermissions(info.Permissions)
if len(info.WorkspaceUsers) > 0 {
printUsers(info.WorkspaceUsers)
}
color.Yellow("\n[i] Expires: Never")
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
permissions := make([]string, 0)
client := analyzers.NewAnalyzeClient(cfg)
bot, err := getBotInfo(client, key)
if err != nil {
return nil, err
}
credPermissions, err := getPermissions(cfg, key)
if err != nil {
return nil, err
}
permissions = append(permissions, credPermissions...)
users, err := getWorkspaceUsers(client, key)
if err != nil {
return nil, fmt.Errorf("error getting user permission: %s", err.Error())
}
// check if email is returned in users to determine permission
for _, user := range users {
if user.Type == "person" {
if user.Person.Email == "" {
permissions = append(permissions, PermissionStrings[ReadUsersWithoutEmail])
} else {
permissions = append(permissions, PermissionStrings[ReadUsersWithEmail])
}
break
}
}
return &SecretInfo{
Bot: bot,
Permissions: permissions,
WorkspaceUsers: users,
}, nil
}
func printPermissions(permissions []string) {
color.Yellow("[i] Permissions:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Permission"})
for _, permission := range permissions {
t.AppendRow(table.Row{color.GreenString(permission)})
}
t.Render()
}
func printUsers(users []user) {
color.Yellow("\n[i] Workspace Users:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"ID", "Name", "Type", "Email"})
for _, user := range users {
t.AppendRow(table.Row{color.GreenString(user.Id), color.GreenString(user.Name), color.GreenString(user.Type), color.GreenString(user.Person.Email)})
}
t.Render()
}
func getBotInfo(client *http.Client, key string) (*bot, error) {
// Create new HTTP request
req, err := http.NewRequest(http.MethodGet, "https://api.notion.com/v1/users/me", http.NoBody)
if err != nil {
return nil, err
}
// Add custom headers if provided
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Notion-Version", "2022-06-28")
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
me := &bot{}
err = json.NewDecoder(resp.Body).Decode(me)
if err != nil {
return nil, err
}
return me, nil
case http.StatusUnauthorized:
return nil, errors.New("invalid API key")
default:
return nil, errors.New("error getting bot info")
}
}
// Decode response body
type usersResponse struct {
Results []user `json:"results"`
}
func getWorkspaceUsers(client *http.Client, key string) ([]user, error) {
// Create new HTTP request
req, err := http.NewRequest(http.MethodGet, "https://api.notion.com/v1/users", http.NoBody)
if err != nil {
return nil, err
}
// Add custom headers if provided
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Notion-Version", "2022-06-28")
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
response := &usersResponse{}
err = json.NewDecoder(resp.Body).Decode(response)
if err != nil {
return nil, err
}
return response.Results, nil
case http.StatusUnauthorized:
return nil, errors.New("invalid API key")
case http.StatusForbidden:
return nil, nil // no permission
case http.StatusNotFound:
return nil, errors.New("workspace not found")
default:
return nil, errors.New("error checking user permissions")
}
}
================================================
FILE: pkg/analyzer/analyzers/notion/notion_test.go
================================================
package notion
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid notion key",
key: testSecrets.MustGetField("NOTION_TOKEN"),
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/notion/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package notion
import "errors"
type Permission int
const (
Invalid Permission = iota
ReadContent Permission = iota
UpdateContent Permission = iota
InsertContent Permission = iota
ReadComments Permission = iota
InsertComments Permission = iota
ReadUsersWithEmail Permission = iota
ReadUsersWithoutEmail Permission = iota
)
var (
PermissionStrings = map[Permission]string{
ReadContent: "read_content",
UpdateContent: "update_content",
InsertContent: "insert_content",
ReadComments: "read_comments",
InsertComments: "insert_comments",
ReadUsersWithEmail: "read_users_with_email",
ReadUsersWithoutEmail: "read_users_without_email",
}
StringToPermission = map[string]Permission{
"read_content": ReadContent,
"update_content": UpdateContent,
"insert_content": InsertContent,
"read_comments": ReadComments,
"insert_comments": InsertComments,
"read_users_with_email": ReadUsersWithEmail,
"read_users_without_email": ReadUsersWithoutEmail,
}
PermissionIDs = map[Permission]int{
ReadContent: 1,
UpdateContent: 2,
InsertContent: 3,
ReadComments: 4,
InsertComments: 5,
ReadUsersWithEmail: 6,
ReadUsersWithoutEmail: 7,
}
IdToPermission = map[int]Permission{
1: ReadContent,
2: UpdateContent,
3: InsertContent,
4: ReadComments,
5: InsertComments,
6: ReadUsersWithEmail,
7: ReadUsersWithoutEmail,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/notion/permissions.yaml
================================================
permissions:
- read_content
- update_content
- insert_content
- read_comments
- insert_comments
- read_users_with_email
- read_users_without_email
================================================
FILE: pkg/analyzer/analyzers/notion/scopes.json
================================================
[
{
"name": "read_content",
"test": {
"endpoint": "https://api.notion.com/v1/pages/`nowaythiscanexist",
"method": "GET",
"valid_status_code": [400],
"invalid_status_code": [403]
}
},
{
"name": "update_content",
"test": {
"endpoint": "https://api.notion.com/v1/pages/`nowaythiscanexist",
"method": "PATCH",
"valid_status_code": [400],
"invalid_status_code": [403]
}
},
{
"name": "insert_content",
"test": {
"endpoint": "https://api.notion.com/v1/pages",
"method": "POST",
"valid_status_code": [400],
"invalid_status_code": [403]
}
},
{
"name": "read_comments",
"test": {
"endpoint": "https://api.notion.com/v1/comments",
"method": "GET",
"valid_status_code": [400],
"invalid_status_code": [403]
}
},
{
"name": "insert_comments",
"test": {
"endpoint": "https://api.notion.com/v1/comments",
"method": "POST",
"valid_status_code": [400],
"invalid_status_code": [403]
}
}
]
================================================
FILE: pkg/analyzer/analyzers/openai/openai.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go openai
package openai
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeOpenAI }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
info, err := AnalyzePermissions(a.Cfg, credInfo["key"])
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *AnalyzerJSON) *analyzers.AnalyzerResult {
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeOpenAI,
Metadata: map[string]any{
"user": info.me.Name,
"email": info.me.Email,
"mfa": strconv.FormatBool(info.me.MfaEnabled),
"is_admin": strconv.FormatBool(info.isAdmin),
"is_restricted": strconv.FormatBool(info.isRestricted),
},
}
perms := convertPermissions(info.isAdmin, info.perms)
for _, org := range info.me.Orgs.Data {
resource := analyzers.Resource{
Name: org.Title,
FullyQualifiedName: org.ID,
Type: "organization",
Metadata: map[string]any{
"description": org.Description,
"user": org.User,
},
}
// Copy each permission into this resource.
result.Bindings = append(result.Bindings, analyzers.BindAllPermissions(resource, perms...)...)
}
return &result
}
func convertPermissions(isAdmin bool, perms []permissionData) []analyzers.Permission {
var permissions []analyzers.Permission
if isAdmin {
permissions = append(permissions, analyzers.Permission{Value: analyzers.FullAccess})
} else {
for _, perm := range flattenPerms(perms...) {
permName := PermissionStrings[perm]
permissions = append(permissions, analyzers.Permission{Value: permName})
}
}
return permissions
}
// flattenPerms takes a slice of permissionData and returns all of the
// individual Permission values in a single slice.
func flattenPerms(perms ...permissionData) []Permission {
var output []Permission
for _, perm := range perms {
output = append(output, perm.permissions...)
}
return output
}
const (
BASE_URL = "https://api.openai.com"
ORGS_ENDPOINT = "/v1/organizations"
ME_ENDPOINT = "/v1/me"
)
type MeJSON struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Phone string `json:"phone_number"`
MfaEnabled bool `json:"mfa_flag_enabled"`
Orgs struct {
Data []struct {
ID string `json:"id"`
Title string `json:"title"`
User string `json:"name"`
Description string `json:"description"`
Personal bool `json:"personal"`
Default bool `json:"is_default"`
Role string `json:"role"`
} `json:"data"`
} `json:"orgs"`
}
type permissionData struct {
name string
endpoints []string
status analyzers.PermissionType
permissions []Permission
}
type AnalyzerJSON struct {
me MeJSON
isAdmin bool
isRestricted bool
perms []permissionData
}
var POST_PAYLOAD = map[string]interface{}{"speed": 1}
func AnalyzeAndPrintPermissions(cfg *config.Config, apiKey string) {
data, err := AnalyzePermissions(cfg, apiKey)
if err != nil {
color.Red("[x] %s", err.Error())
return
}
color.Green("[!] Valid OpenAI Token\n\n")
printAPIKeyType(apiKey)
printData(data.me)
if data.isAdmin {
color.Green("[!] Admin API Key. All permissions available.")
} else if data.isRestricted {
color.Yellow("[!] Restricted API Key. Limited permissions available.")
printPermissions(data.perms, cfg.ShowAll)
}
}
// AnalyzePermissions will analyze the permissions of an OpenAI API key
func AnalyzePermissions(cfg *config.Config, key string) (*AnalyzerJSON, error) {
data := AnalyzerJSON{
isAdmin: false,
isRestricted: false,
}
meJSON, err := getUserData(cfg, key)
if err != nil {
return nil, err
}
data.me = meJSON
isAdmin, err := checkAdminKey(cfg, key)
if err != nil {
return nil, err
}
if isAdmin {
data.isAdmin = true
} else {
data.isRestricted = true
if err := analyzeScopes(key); err != nil {
return nil, err
}
data.perms = getPermissions()
}
return &data, nil
}
func analyzeScopes(key string) error {
for _, scope := range SCOPES {
if err := scope.RunTests(key); err != nil {
return err
}
}
return nil
}
func openAIRequest(cfg *config.Config, method string, url string, key string, data map[string]interface{}) ([]byte, *http.Response, error) {
var inBody io.Reader
if data != nil {
jsonData, err := json.Marshal(data)
if err != nil {
return nil, nil, err
}
inBody = bytes.NewBuffer(jsonData)
}
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest(method, url, inBody)
if err != nil {
return nil, nil, err
}
req.Header.Add("Authorization", "Bearer "+key)
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
outBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
return outBody, resp, nil
}
func checkAdminKey(cfg *config.Config, key string) (bool, error) {
// Check for all permissions
//nolint:bodyclose
_, resp, err := openAIRequest(cfg, "GET", BASE_URL+ORGS_ENDPOINT, key, nil)
if err != nil {
return false, err
}
switch resp.StatusCode {
case 200:
return true, nil
case 403:
return false, nil
default:
return false, err
}
}
func getUserData(cfg *config.Config, key string) (MeJSON, error) {
var meJSON MeJSON
//nolint:bodyclose
me, resp, err := openAIRequest(cfg, "GET", BASE_URL+ME_ENDPOINT, key, nil)
if err != nil {
return meJSON, err
}
if resp.StatusCode != 200 {
return meJSON, fmt.Errorf("invalid OpenAI token")
}
// Marshall me into meJSON struct
if err := json.Unmarshal(me, &meJSON); err != nil {
return meJSON, err
}
return meJSON, nil
}
func printAPIKeyType(apiKey string) {
if strings.Contains(apiKey, "-svcacct-") {
color.Yellow("[i] Service Account API Key\n")
} else if strings.Contains(apiKey, "-admin-") {
color.Yellow("[i] Admin API Key\n")
} else {
color.Yellow("[i] Project/Org API Key\n")
}
}
func printData(meJSON MeJSON) {
if meJSON.Name != "" && meJSON.Email != "" {
userTable := table.NewWriter()
userTable.SetOutputMirror(os.Stdout)
color.Green("[i] User Information")
userTable.AppendHeader(table.Row{"UserID", "User", "Email", "Phone", "MFA Enabled"})
userTable.AppendRow(table.Row{meJSON.ID, meJSON.Name, meJSON.Email, meJSON.Phone, meJSON.MfaEnabled})
userTable.Render()
} else {
color.Yellow("[!] No User Information Available")
}
if len(meJSON.Orgs.Data) > 0 {
orgTable := table.NewWriter()
orgTable.SetOutputMirror(os.Stdout)
color.Green("[i] Organizations Information")
orgTable.AppendHeader(table.Row{"Org ID", "Title", "User", "Default", "Role"})
for _, org := range meJSON.Orgs.Data {
orgTable.AppendRow(table.Row{org.ID, fmt.Sprintf("%s (%s)", org.Title, org.Description), org.User, org.Default, org.Role})
}
orgTable.Render()
} else {
color.Yellow("[!] No Organizations Information Available")
}
}
func stringifyPermissionStatus(scope OpenAIScope) ([]Permission, analyzers.PermissionType) {
readStatus := false
writeStatus := false
errors := false
for _, test := range scope.ReadTests {
if test.Type == analyzers.READ {
readStatus = test.Status.Value
}
if test.Status.IsError {
errors = true
}
}
for _, test := range scope.WriteTests {
if test.Type == analyzers.WRITE {
writeStatus = test.Status.Value
}
if test.Status.IsError {
errors = true
}
}
if errors {
return nil, analyzers.ERROR
}
if readStatus && writeStatus {
return []Permission{scope.ReadPermission, scope.WritePermission}, analyzers.READ_WRITE
} else if readStatus {
return []Permission{scope.ReadPermission}, analyzers.READ
} else if writeStatus {
return []Permission{scope.WritePermission}, analyzers.WRITE
} else {
return nil, analyzers.NONE
}
}
func getPermissions() []permissionData {
var perms []permissionData
for _, scope := range SCOPES {
permissions, status := stringifyPermissionStatus(scope)
perms = append(perms, permissionData{
name: scope.Endpoints[0], // Using the first endpoint as the name for simplicity
endpoints: scope.Endpoints,
status: status,
permissions: permissions,
})
}
return perms
}
func printPermissions(perms []permissionData, showAll bool) {
fmt.Print("\n\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "Endpoints", "Permission"})
for _, perm := range perms {
if showAll || perm.status != analyzers.NONE {
t.AppendRow([]any{perm.name, perm.endpoints[0], perm.status})
for i := 1; i < len(perm.endpoints); i++ {
t.AppendRow([]any{"", perm.endpoints[i], perm.status})
}
}
}
t.Render()
fmt.Print("\n\n")
}
================================================
FILE: pkg/analyzer/analyzers/openai/openai_test.go
================================================
package openai
import (
_ "embed"
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed result_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
want []byte
wantErr bool
}{
{
name: "valid OpenAI key",
key: testSecrets.MustGetField("OPENAI_VERIFIED"),
want: expectedOutput,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/openai/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package openai
import "errors"
type Permission int
const (
Invalid Permission = iota
ModelsRead Permission = iota
ModelCapabilitiesWrite Permission = iota
AssistantsRead Permission = iota
AssistantsWrite Permission = iota
ThreadsRead Permission = iota
ThreadsWrite Permission = iota
FineTuningRead Permission = iota
FineTuningWrite Permission = iota
FilesRead Permission = iota
FilesWrite Permission = iota
EvalsRead Permission = iota
EvalsWrite Permission = iota
ResponsesRead Permission = iota
ResponsesWrite Permission = iota
)
var (
PermissionStrings = map[Permission]string{
ModelsRead: "models:read",
ModelCapabilitiesWrite: "model_capabilities:write",
AssistantsRead: "assistants:read",
AssistantsWrite: "assistants:write",
ThreadsRead: "threads:read",
ThreadsWrite: "threads:write",
FineTuningRead: "fine_tuning:read",
FineTuningWrite: "fine_tuning:write",
FilesRead: "files:read",
FilesWrite: "files:write",
EvalsRead: "evals:read",
EvalsWrite: "evals:write",
ResponsesRead: "responses:read",
ResponsesWrite: "responses:write",
}
StringToPermission = map[string]Permission{
"models:read": ModelsRead,
"model_capabilities:write": ModelCapabilitiesWrite,
"assistants:read": AssistantsRead,
"assistants:write": AssistantsWrite,
"threads:read": ThreadsRead,
"threads:write": ThreadsWrite,
"fine_tuning:read": FineTuningRead,
"fine_tuning:write": FineTuningWrite,
"files:read": FilesRead,
"files:write": FilesWrite,
"evals:read": EvalsRead,
"evals:write": EvalsWrite,
"responses:read": ResponsesRead,
"responses:write": ResponsesWrite,
}
PermissionIDs = map[Permission]int{
ModelsRead: 1,
ModelCapabilitiesWrite: 2,
AssistantsRead: 3,
AssistantsWrite: 4,
ThreadsRead: 5,
ThreadsWrite: 6,
FineTuningRead: 7,
FineTuningWrite: 8,
FilesRead: 9,
FilesWrite: 10,
EvalsRead: 11,
EvalsWrite: 12,
ResponsesRead: 13,
ResponsesWrite: 14,
}
IdToPermission = map[int]Permission{
1: ModelsRead,
2: ModelCapabilitiesWrite,
3: AssistantsRead,
4: AssistantsWrite,
5: ThreadsRead,
6: ThreadsWrite,
7: FineTuningRead,
8: FineTuningWrite,
9: FilesRead,
10: FilesWrite,
11: EvalsRead,
12: EvalsWrite,
13: ResponsesRead,
14: ResponsesWrite,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/openai/permissions.yaml
================================================
permissions:
- models:read
- model_capabilities:write
- assistants:read
- assistants:write
- threads:read
- threads:write
- fine_tuning:read
- fine_tuning:write
- files:read
- files:write
- evals:read
- evals:write
- responses:read
- responses:write
================================================
FILE: pkg/analyzer/analyzers/openai/result_output.json
================================================
{
"AnalyzerType": 13,
"Bindings": [
{
"Resource": {
"Name": "Truffle Security Co",
"FullyQualifiedName": "org-n56tuYdSewh06PEGJZC0xWHf",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "truffle-security-co"
},
"Parent": null
},
"Permission": {
"Value": "models:read",
"Parent": null
}
},
{
"Resource": {
"Name": "Truffle Security Co",
"FullyQualifiedName": "org-n56tuYdSewh06PEGJZC0xWHf",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "truffle-security-co"
},
"Parent": null
},
"Permission": {
"Value": "model_capabilities:write",
"Parent": null
}
},
{
"Resource": {
"Name": "Truffle Security Co",
"FullyQualifiedName": "org-n56tuYdSewh06PEGJZC0xWHf",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "truffle-security-co"
},
"Parent": null
},
"Permission": {
"Value": "assistants:read",
"Parent": null
}
},
{
"Resource": {
"Name": "Truffle Security Co",
"FullyQualifiedName": "org-n56tuYdSewh06PEGJZC0xWHf",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "truffle-security-co"
},
"Parent": null
},
"Permission": {
"Value": "assistants:write",
"Parent": null
}
},
{
"Resource": {
"Name": "Truffle Security Co",
"FullyQualifiedName": "org-n56tuYdSewh06PEGJZC0xWHf",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "truffle-security-co"
},
"Parent": null
},
"Permission": {
"Value": "threads:read",
"Parent": null
}
},
{
"Resource": {
"Name": "Truffle Security Co",
"FullyQualifiedName": "org-n56tuYdSewh06PEGJZC0xWHf",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "truffle-security-co"
},
"Parent": null
},
"Permission": {
"Value": "threads:write",
"Parent": null
}
},
{
"Resource": {
"Name": "Truffle Security Co",
"FullyQualifiedName": "org-n56tuYdSewh06PEGJZC0xWHf",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "truffle-security-co"
},
"Parent": null
},
"Permission": {
"Value": "fine_tuning:read",
"Parent": null
}
},
{
"Resource": {
"Name": "Truffle Security Co",
"FullyQualifiedName": "org-n56tuYdSewh06PEGJZC0xWHf",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "truffle-security-co"
},
"Parent": null
},
"Permission": {
"Value": "fine_tuning:write",
"Parent": null
}
},
{
"Resource": {
"Name": "Truffle Security Co",
"FullyQualifiedName": "org-n56tuYdSewh06PEGJZC0xWHf",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "truffle-security-co"
},
"Parent": null
},
"Permission": {
"Value": "files:read",
"Parent": null
}
},
{
"Resource": {
"Name": "Truffle Security Co",
"FullyQualifiedName": "org-n56tuYdSewh06PEGJZC0xWHf",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "truffle-security-co"
},
"Parent": null
},
"Permission": {
"Value": "files:write",
"Parent": null
}
},
{
"Resource": {
"Name": "Truffle Security Co",
"FullyQualifiedName": "org-n56tuYdSewh06PEGJZC0xWHf",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "truffle-security-co"
},
"Parent": null
},
"Permission": {
"Value": "evals:read",
"Parent": null
}
},
{
"Resource": {
"Name": "Truffle Security Co",
"FullyQualifiedName": "org-n56tuYdSewh06PEGJZC0xWHf",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "truffle-security-co"
},
"Parent": null
},
"Permission": {
"Value": "evals:write",
"Parent": null
}
},
{
"Resource": {
"Name": "Truffle Security Co",
"FullyQualifiedName": "org-n56tuYdSewh06PEGJZC0xWHf",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "truffle-security-co"
},
"Parent": null
},
"Permission": {
"Value": "responses:read",
"Parent": null
}
},
{
"Resource": {
"Name": "Truffle Security Co",
"FullyQualifiedName": "org-n56tuYdSewh06PEGJZC0xWHf",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "truffle-security-co"
},
"Parent": null
},
"Permission": {
"Value": "responses:write",
"Parent": null
}
},
{
"Resource": {
"Name": "Personal",
"FullyQualifiedName": "org-S2T2qOGM1KofMLUxb9rt7eV0",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "user-ohfap0ky8lkatw97iskuhghv"
},
"Parent": null
},
"Permission": {
"Value": "models:read",
"Parent": null
}
},
{
"Resource": {
"Name": "Personal",
"FullyQualifiedName": "org-S2T2qOGM1KofMLUxb9rt7eV0",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "user-ohfap0ky8lkatw97iskuhghv"
},
"Parent": null
},
"Permission": {
"Value": "model_capabilities:write",
"Parent": null
}
},
{
"Resource": {
"Name": "Personal",
"FullyQualifiedName": "org-S2T2qOGM1KofMLUxb9rt7eV0",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "user-ohfap0ky8lkatw97iskuhghv"
},
"Parent": null
},
"Permission": {
"Value": "assistants:read",
"Parent": null
}
},
{
"Resource": {
"Name": "Personal",
"FullyQualifiedName": "org-S2T2qOGM1KofMLUxb9rt7eV0",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "user-ohfap0ky8lkatw97iskuhghv"
},
"Parent": null
},
"Permission": {
"Value": "assistants:write",
"Parent": null
}
},
{
"Resource": {
"Name": "Personal",
"FullyQualifiedName": "org-S2T2qOGM1KofMLUxb9rt7eV0",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "user-ohfap0ky8lkatw97iskuhghv"
},
"Parent": null
},
"Permission": {
"Value": "threads:read",
"Parent": null
}
},
{
"Resource": {
"Name": "Personal",
"FullyQualifiedName": "org-S2T2qOGM1KofMLUxb9rt7eV0",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "user-ohfap0ky8lkatw97iskuhghv"
},
"Parent": null
},
"Permission": {
"Value": "threads:write",
"Parent": null
}
},
{
"Resource": {
"Name": "Personal",
"FullyQualifiedName": "org-S2T2qOGM1KofMLUxb9rt7eV0",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "user-ohfap0ky8lkatw97iskuhghv"
},
"Parent": null
},
"Permission": {
"Value": "fine_tuning:read",
"Parent": null
}
},
{
"Resource": {
"Name": "Personal",
"FullyQualifiedName": "org-S2T2qOGM1KofMLUxb9rt7eV0",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "user-ohfap0ky8lkatw97iskuhghv"
},
"Parent": null
},
"Permission": {
"Value": "fine_tuning:write",
"Parent": null
}
},
{
"Resource": {
"Name": "Personal",
"FullyQualifiedName": "org-S2T2qOGM1KofMLUxb9rt7eV0",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "user-ohfap0ky8lkatw97iskuhghv"
},
"Parent": null
},
"Permission": {
"Value": "files:read",
"Parent": null
}
},
{
"Resource": {
"Name": "Personal",
"FullyQualifiedName": "org-S2T2qOGM1KofMLUxb9rt7eV0",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "user-ohfap0ky8lkatw97iskuhghv"
},
"Parent": null
},
"Permission": {
"Value": "files:write",
"Parent": null
}
},
{
"Resource": {
"Name": "Personal",
"FullyQualifiedName": "org-S2T2qOGM1KofMLUxb9rt7eV0",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "user-ohfap0ky8lkatw97iskuhghv"
},
"Parent": null
},
"Permission": {
"Value": "evals:read",
"Parent": null
}
},
{
"Resource": {
"Name": "Personal",
"FullyQualifiedName": "org-S2T2qOGM1KofMLUxb9rt7eV0",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "user-ohfap0ky8lkatw97iskuhghv"
},
"Parent": null
},
"Permission": {
"Value": "evals:write",
"Parent": null
}
},
{
"Resource": {
"Name": "Personal",
"FullyQualifiedName": "org-S2T2qOGM1KofMLUxb9rt7eV0",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "user-ohfap0ky8lkatw97iskuhghv"
},
"Parent": null
},
"Permission": {
"Value": "responses:read",
"Parent": null
}
},
{
"Resource": {
"Name": "Personal",
"FullyQualifiedName": "org-S2T2qOGM1KofMLUxb9rt7eV0",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "user-ohfap0ky8lkatw97iskuhghv"
},
"Parent": null
},
"Permission": {
"Value": "responses:write",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": {
"email": "dustin@trufflesec.com",
"is_admin": "false",
"is_restricted": "true",
"mfa": "true",
"user": "Dustin Decker"
}
}
================================================
FILE: pkg/analyzer/analyzers/openai/scopes.go
================================================
package openai
import (
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
)
type OpenAIScope struct {
ReadTests []analyzers.HttpStatusTest
WriteTests []analyzers.HttpStatusTest
Endpoints []string
ReadPermission Permission
WritePermission Permission
}
func (s *OpenAIScope) RunTests(key string) error {
headers := map[string]string{
"Authorization": "Bearer " + key,
"Content-Type": "application/json",
}
for i := range s.ReadTests {
test := &s.ReadTests[i]
if err := test.RunTest(headers); err != nil {
return err
}
}
for i := range s.WriteTests {
test := &s.WriteTests[i]
if err := test.RunTest(headers); err != nil {
return err
}
}
return nil
}
var SCOPES = []OpenAIScope{
{
ReadTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/models", Method: "GET", Valid: []int{200}, Invalid: []int{403}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/models"},
ReadPermission: ModelsRead,
},
{
WriteTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/images/generations", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{400}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/audio", "/v1/chat/completions", "/v1/embeddings", "/v1/images", "/v1/moderations"},
WritePermission: ModelCapabilitiesWrite,
},
{
ReadTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/assistants", Method: "GET", Valid: []int{400}, Invalid: []int{401}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
},
WriteTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/assistants", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{400}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/assistants"},
ReadPermission: AssistantsRead,
WritePermission: AssistantsWrite,
},
{
ReadTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/threads/1", Method: "GET", Valid: []int{400}, Invalid: []int{401}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
},
WriteTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/threads", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{400}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/threads"},
ReadPermission: ThreadsRead,
WritePermission: ThreadsWrite,
},
{
ReadTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/fine_tuning/jobs", Method: "GET", Valid: []int{200}, Invalid: []int{401}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
},
WriteTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/fine_tuning/jobs", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{400}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/fine_tuning"},
ReadPermission: FineTuningRead,
WritePermission: FineTuningWrite,
},
{
ReadTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/files", Method: "GET", Valid: []int{200}, Invalid: []int{401}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
},
WriteTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/files", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{415}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/files"},
ReadPermission: FilesRead,
WritePermission: FilesWrite,
},
{
ReadTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/evals", Method: "GET", Valid: []int{200}, Invalid: []int{401}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
},
WriteTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/evals", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{400}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/evals"},
ReadPermission: EvalsRead,
WritePermission: EvalsWrite,
},
{
ReadTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/responses/1", Method: "GET", Valid: []int{400}, Invalid: []int{401}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
},
WriteTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/responses", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{400}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/responses"},
ReadPermission: ResponsesRead,
WritePermission: ResponsesWrite,
},
}
================================================
FILE: pkg/analyzer/analyzers/opsgenie/opsgenie.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go opsgenie
package opsgenie
import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeOpsgenie }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, errors.New("missing key in credInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeOpsgenie,
Metadata: nil,
Bindings: make([]analyzers.Binding, len(info.Permissions)),
UnboundedResources: make([]analyzers.Resource, len(info.Users)),
}
// Opsgenie has API integrations, so the key does not belong
// to a particular user or account, it itself is a resource
resource := analyzers.Resource{
Name: "Opsgenie API Integration Key",
FullyQualifiedName: "Opsgenie API Integration Key",
Type: "API Key",
Metadata: map[string]any{
"expires": "never",
},
}
for idx, permission := range info.Permissions {
result.Bindings[idx] = analyzers.Binding{
Resource: resource,
Permission: analyzers.Permission{
Value: permission,
},
}
}
// We can find list of users in the current account
// if the API key has Configuration Access, so these can be
// unbounded resources
for idx, user := range info.Users {
result.UnboundedResources[idx] = analyzers.Resource{
Name: user.FullName,
FullyQualifiedName: user.Username,
Type: "user",
Metadata: map[string]any{
"username": user.Username,
"role": user.Role.Name,
},
}
}
return &result
}
//go:embed scopes.json
var scopesConfig []byte
type User struct {
FullName string `json:"fullName"`
Username string `json:"username"`
Role struct {
Name string `json:"name"`
} `json:"role"`
}
type UsersJSON struct {
Users []User `json:"data"`
}
type HttpStatusTest struct {
Endpoint string `json:"endpoint"`
Method string `json:"method"`
Payload interface{} `json:"payload"`
ValidStatuses []int `json:"valid_status_code"`
InvalidStatuses []int `json:"invalid_status_code"`
}
func StatusContains(status int, vals []int) bool {
for _, v := range vals {
if status == v {
return true
}
}
return false
}
func (h *HttpStatusTest) RunTest(cfg *config.Config, headers map[string]string) (bool, error) {
// If body data, marshal to JSON
var data io.Reader
if h.Payload != nil {
jsonData, err := json.Marshal(h.Payload)
if err != nil {
return false, err
}
data = bytes.NewBuffer(jsonData)
}
// Create new HTTP request
var client *http.Client
// Non-safe Opsgenie APIs are asynchronous and always return 202 if credential has the permission.
// For Safe API Methods, use the restricted client
if analyzers.IsMethodSafe(h.Method) {
client = analyzers.NewAnalyzeClient(cfg)
} else {
client = analyzers.NewAnalyzeClientUnrestricted(cfg)
}
req, err := http.NewRequest(h.Method, h.Endpoint, data)
if err != nil {
return false, err
}
// Add custom headers if provided
for key, value := range headers {
req.Header.Set(key, value)
}
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// Check response status code
switch {
case StatusContains(resp.StatusCode, h.ValidStatuses):
return true, nil
case StatusContains(resp.StatusCode, h.InvalidStatuses):
return false, nil
default:
return false, errors.New("error checking response status code")
}
}
type Scope struct {
Name string `json:"name"`
HttpTest HttpStatusTest `json:"test"`
}
func readInScopes() ([]Scope, error) {
var scopes []Scope
if err := json.Unmarshal(scopesConfig, &scopes); err != nil {
return nil, err
}
return scopes, nil
}
func checkPermissions(cfg *config.Config, key string) ([]string, error) {
scopes, err := readInScopes()
if err != nil {
return nil, fmt.Errorf("reading in scopes: %w", err)
}
permissions := make([]string, 0)
for _, scope := range scopes {
status, err := scope.HttpTest.RunTest(cfg, map[string]string{"Authorization": "GenieKey " + key})
if err != nil {
return nil, fmt.Errorf("running test: %w", err)
}
if status {
permissions = append(permissions, scope.Name)
}
}
return permissions, nil
}
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
func getUserList(cfg *config.Config, key string) ([]User, error) {
// Create new HTTP request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://api.opsgenie.com/v2/users", nil)
if err != nil {
return nil, err
}
// Add custom headers if provided
req.Header.Set("Authorization", "GenieKey "+key)
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Decode response body
var userList UsersJSON
err = json.NewDecoder(resp.Body).Decode(&userList)
if err != nil {
return nil, err
}
return userList.Users, nil
}
type SecretInfo struct {
Users []User
Permissions []string
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error : %s", err.Error())
return
}
color.Green("[!] Valid OpsGenie API key\n\n")
printPermissions(info.Permissions)
if len(info.Users) > 0 {
printUsers(info.Users)
}
color.Yellow("\n[i] Expires: Never")
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
var info = &SecretInfo{}
permissions, err := checkPermissions(cfg, key)
if err != nil {
return nil, err
}
if len(permissions) == 0 {
return nil, fmt.Errorf("invalid OpsGenie API key")
}
info.Permissions = permissions
if contains(permissions, "configuration_access") {
users, err := getUserList(cfg, key)
if err != nil {
return nil, fmt.Errorf("getting user list: %w", err)
}
info.Users = users
}
return info, nil
}
func printPermissions(permissions []string) {
color.Yellow("[i] Permissions:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Permission"})
for _, permission := range permissions {
t.AppendRow(table.Row{color.GreenString(permission)})
}
t.Render()
}
func printUsers(users []User) {
color.Green("\n[i] Users:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Name", "Username", "Role"})
for _, user := range users {
t.AppendRow(table.Row{color.GreenString(user.FullName), color.GreenString(user.Username), color.GreenString(user.Role.Name)})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/opsgenie/opsgenie_test.go
================================================
package opsgenie
import (
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
key := testSecrets.MustGetField("OPSGENIE")
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid Opsgenie API key",
key: key,
want: `{
"AnalyzerType": 11,
"Bindings": [
{
"Resource": {
"Name": "Opsgenie API Integration Key",
"FullyQualifiedName": "Opsgenie API Integration Key",
"Type": "API Key",
"Metadata": {
"expires": "never"
},
"Parent": null
},
"Permission": {
"Value": "configuration_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Opsgenie API Integration Key",
"FullyQualifiedName": "Opsgenie API Integration Key",
"Type": "API Key",
"Metadata": {
"expires": "never"
},
"Parent": null
},
"Permission": {
"Value": "read",
"Parent": null
}
},
{
"Resource": {
"Name": "Opsgenie API Integration Key",
"FullyQualifiedName": "Opsgenie API Integration Key",
"Type": "API Key",
"Metadata": {
"expires": "never"
},
"Parent": null
},
"Permission": {
"Value": "delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Opsgenie API Integration Key",
"FullyQualifiedName": "Opsgenie API Integration Key",
"Type": "API Key",
"Metadata": {
"expires": "never"
},
"Parent": null
},
"Permission": {
"Value": "create_and_update",
"Parent": null
}
}
],
"UnboundedResources": [
{
"Name": "John Scanner",
"FullyQualifiedName": "secretscanner02@zohomail.com",
"Type": "user",
"Metadata": {
"role": "Owner",
"username": "secretscanner02@zohomail.com"
},
"Parent": null
}
],
"Metadata": null
}`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/opsgenie/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package opsgenie
import "errors"
type Permission int
const (
Invalid Permission = iota
ConfigurationAccess Permission = iota
Read Permission = iota
Delete Permission = iota
CreateAndUpdate Permission = iota
)
var (
PermissionStrings = map[Permission]string{
ConfigurationAccess: "configuration_access",
Read: "read",
Delete: "delete",
CreateAndUpdate: "create_and_update",
}
StringToPermission = map[string]Permission{
"configuration_access": ConfigurationAccess,
"read": Read,
"delete": Delete,
"create_and_update": CreateAndUpdate,
}
PermissionIDs = map[Permission]int{
ConfigurationAccess: 1,
Read: 2,
Delete: 3,
CreateAndUpdate: 4,
}
IdToPermission = map[int]Permission{
1: ConfigurationAccess,
2: Read,
3: Delete,
4: CreateAndUpdate,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/opsgenie/permissions.yaml
================================================
permissions:
- configuration_access
- read
- delete
- create_and_update
================================================
FILE: pkg/analyzer/analyzers/opsgenie/scopes.json
================================================
[
{
"name": "configuration_access",
"test": {
"endpoint": "https://api.opsgenie.com/v2/account",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "read",
"test": {
"endpoint": "https://api.opsgenie.com/v2/alerts",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "delete",
"test": {
"endpoint": "https://api.opsgenie.com/v2/alerts/`nowaythiscanexist",
"method": "DELETE",
"valid_status_code": [202],
"invalid_status_code": [403]
}
},
{
"name": "create_and_update",
"test": {
"endpoint": "https://api.opsgenie.com/v2/alerts/`nowaycanthisexist/message",
"method": "PUT",
"valid_status_code": [400],
"invalid_status_code": [403]
}
}
]
================================================
FILE: pkg/analyzer/analyzers/plaid/expected_output.json
================================================
{"AnalyzerType":33,"Bindings":[{"Resource":{"Name":"Assets","FullyQualifiedName":"zkjAkPqvmyikkkyJMbBjCnEPEAR5kjio3KAM4/product/assets","Type":"product","Metadata":{"productDesc":"Request, retrieve and share detailed reports of financial assets and account history"},"Parent":null},"Permission":{"Value":"write","Parent":null}},{"Resource":{"Name":"Auth","FullyQualifiedName":"zkjAkPqvmyikkkyJMbBjCnEPEAR5kjio3KAM4/product/auth","Type":"product","Metadata":{"productDesc":"Retrieve account and routing numbers"},"Parent":null},"Permission":{"Value":"read","Parent":null}},{"Resource":{"Name":"Identity","FullyQualifiedName":"zkjAkPqvmyikkkyJMbBjCnEPEAR5kjio3KAM4/product/identity","Type":"product","Metadata":{"productDesc":"Access personal identity information like name, phone, address, and email"},"Parent":null},"Permission":{"Value":"read","Parent":null}},{"Resource":{"Name":"Investments","FullyQualifiedName":"zkjAkPqvmyikkkyJMbBjCnEPEAR5kjio3KAM4/product/investments","Type":"product","Metadata":{"productDesc":"Retrieve holdings, balances, and historical investment transactions"},"Parent":null},"Permission":{"Value":"read","Parent":null}},{"Resource":{"Name":"Liabilities","FullyQualifiedName":"zkjAkPqvmyikkkyJMbBjCnEPEAR5kjio3KAM4/product/liabilities","Type":"product","Metadata":{"productDesc":"Access detailed information about loans, credit cards, and other liabilities"},"Parent":null},"Permission":{"Value":"write","Parent":null}},{"Resource":{"Name":"Transactions","FullyQualifiedName":"zkjAkPqvmyikkkyJMbBjCnEPEAR5kjio3KAM4/product/transactions","Type":"product","Metadata":{"productDesc":"Retrieve, filter, and analyze categorized transaction history"},"Parent":null},"Permission":{"Value":"read","Parent":null}},{"Resource":{"Name":"Transfer","FullyQualifiedName":"zkjAkPqvmyikkkyJMbBjCnEPEAR5kjio3KAM4/product/transfer","Type":"product","Metadata":{"productDesc":"Initiate, manage, and track bank transfers"},"Parent":null},"Permission":{"Value":"write","Parent":null}}],"UnboundedResources":[{"Name":"Plaid Checking","FullyQualifiedName":"K1xy1qQJn8u555qNpjrbFxba95ydRxCRpw1nM","Type":"account","Metadata":{"officialName":"Plaid Gold Standard 0% Interest Checking"},"Parent":null},{"Name":"Plaid Saving","FullyQualifiedName":"rpBKpzVvJ8S333ZPGE8bcg8PvJbG4gC7JK1on","Type":"account","Metadata":{"officialName":"Plaid Silver Standard 0.1% Interest Saving"},"Parent":null},{"Name":"Plaid CD","FullyQualifiedName":"zkjAkPqvmyikkkyJMbBjCnEP1lepvnClNPQq1","Type":"account","Metadata":{"officialName":"Plaid Bronze Standard 0.2% Interest CD"},"Parent":null},{"Name":"Plaid Credit Card","FullyQualifiedName":"B4Vv4GxJReIEEEj6Lz9viM6XpLBRzMT4MegZn","Type":"account","Metadata":{"officialName":"Plaid Diamond 12.5% APR Interest Credit Card"},"Parent":null},{"Name":"Plaid Money Market","FullyQualifiedName":"3y8LyjgMKltRRR1aNd5zHEGl8Q3LWEHZloEPn","Type":"account","Metadata":{"officialName":"Plaid Platinum Standard 1.85% Interest Money Market"},"Parent":null},{"Name":"Plaid IRA","FullyQualifiedName":"ed9ydAVLvNUjjjPMG5N6CXN6yWry9jtr96mEk","Type":"account","Metadata":{"officialName":""},"Parent":null},{"Name":"Plaid 401k","FullyQualifiedName":"QEj7ExolJbi555xEeqBbFjE4qJ3qQbtwmyDzD","Type":"account","Metadata":{"officialName":""},"Parent":null},{"Name":"Plaid Student Loan","FullyQualifiedName":"ZvopvQVaqlSKKKQak4lrCgl14Wd4yXfeqQGz1","Type":"account","Metadata":{"officialName":""},"Parent":null},{"Name":"Plaid Mortgage","FullyQualifiedName":"MpaRprMJ7WS555r49WVdFabjPmyPeDUL6n1zX","Type":"account","Metadata":{"officialName":""},"Parent":null},{"Name":"Plaid HSA","FullyQualifiedName":"1boLbNpQlXSqqqoM7e53trlEbaAb91FpjWr3X","Type":"account","Metadata":{"officialName":"Plaid Cares Health Savings Account"},"Parent":null},{"Name":"Plaid Cash Management","FullyQualifiedName":"LLdQLKZJVEH555KNbnxaFd3RwEawW9ukjDEoj","Type":"account","Metadata":{"officialName":"Plaid Growth Cash Management"},"Parent":null},{"Name":"Plaid Business Credit Card","FullyQualifiedName":"p1rl1KVv5ouzzzkMEVo1s5vBPXyPo9UpoRzkM","Type":"account","Metadata":{"officialName":"Plaid Platinum Small Business Credit Card"},"Parent":null}],"Metadata":null}
================================================
FILE: pkg/analyzer/analyzers/plaid/models.go
================================================
package plaid
type account struct {
AccountID string `json:"account_id"`
Name string `json:"name"`
OfficialName string `json:"official_name"`
Subtype string `json:"subtype"`
Type string `json:"type"`
}
type item struct {
Products []string `json:"products"`
ItemID string `json:"item_id"`
}
type accountsResponse struct {
Accounts []account `json:"accounts"`
Item item `json:"item"`
}
type secretInfo struct {
Item item
Accounts []account
Environment string
}
================================================
FILE: pkg/analyzer/analyzers/plaid/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package plaid
import "errors"
type Permission int
const (
Invalid Permission = iota
Read Permission = iota
Write Permission = iota
)
var (
PermissionStrings = map[Permission]string{
Read: "read",
Write: "write",
}
StringToPermission = map[string]Permission{
"read": Read,
"write": Write,
}
PermissionIDs = map[Permission]int{
Read: 1,
Write: 2,
}
IdToPermission = map[int]Permission{
1: Read,
2: Write,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/plaid/permissions.yaml
================================================
permissions:
- read
- write
================================================
FILE: pkg/analyzer/analyzers/plaid/plaid.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go plaid
package plaid
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypePlaid
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
secret, exist := credInfo["secret"]
if !exist {
return nil, errors.New("secret not found in credentials info")
}
clientID, exist := credInfo["id"]
if !exist {
return nil, errors.New("id not found in credentials info")
}
accessToken, exist := credInfo["token"]
if !exist {
return nil, errors.New("token not found in credentials info")
}
info, err := AnalyzePermissions(a.Cfg, secret, clientID, accessToken)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, secret string, clientID string, accessToken string) {
info, err := AnalyzePermissions(cfg, secret, clientID, accessToken)
if err != nil {
color.Red("[x] Invalid Plaid API key\n")
color.Red("[x] Error : %s", err.Error())
return
}
if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}
color.Green("[i] Valid Plaid API Credentials\n")
color.Yellow("\n[i] Environment: %s", info.Environment)
if info.Environment == "sandbox" {
color.Cyan("Credentials are for Sandbox environment. All resources found are simulated and not real data.\n")
}
printAccountsAndProducts(info)
}
func AnalyzePermissions(cfg *config.Config, secret string, clientId string, accessToken string) (*secretInfo, error) {
environment := "sandbox"
if strings.Contains(accessToken, "production") {
environment = "production"
}
// Plaid API uses POST requests for all requests, so we need to use an unrestricted client
client := analyzers.NewAnalyzeClientUnrestricted(cfg)
var secretInfo = &secretInfo{}
secretInfo.Environment = environment
resp, err := getPlaidAccounts(client, clientId, secret, accessToken, environment)
if err != nil {
return nil, err
}
secretInfo.Item = resp.Item
secretInfo.Accounts = resp.Accounts
return secretInfo, nil
}
func getPlaidAccounts(client *http.Client, clientID string, secret string, accessToken string, environment string) (*accountsResponse, error) {
body := map[string]interface{}{
"client_id": clientID,
"secret": secret,
"access_token": accessToken,
}
url := "https://" + environment + ".plaid.com/accounts/get"
jsonBody, _ := json.Marshal(body)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("received non-OK HTTP status: %d", resp.StatusCode)
}
var accounts accountsResponse
if err := json.NewDecoder(resp.Body).Decode(&accounts); err != nil {
return nil, err
}
return &accounts, nil
}
func secretInfoToAnalyzerResult(info *secretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
itemID := info.Item.ItemID
userProducts := info.Item.Products
userAccounts := info.Accounts
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypePlaid,
Metadata: nil,
Bindings: make([]analyzers.Binding, len(userProducts)),
UnboundedResources: make([]analyzers.Resource, len(userAccounts)),
}
for idx, productName := range userProducts {
product, ok := GetProductByName(productName)
if !ok {
continue
}
result.Bindings[idx] = analyzers.Binding{
Resource: analyzers.Resource{
Name: product.DisplayName,
FullyQualifiedName: itemID + "/product/" + product.Name,
Type: "product",
Metadata: map[string]any{
"productDesc": product.Description,
},
},
Permission: analyzers.Permission{
Value: PermissionStrings[product.PermissionLevel],
},
}
}
for idx, account := range info.Accounts {
result.UnboundedResources[idx] = analyzers.Resource{
Name: account.Name,
FullyQualifiedName: account.AccountID,
Type: "account",
Metadata: map[string]any{
"officialName": account.OfficialName,
},
}
}
return &result
}
func printAccountsAndProducts(info *secretInfo) {
userProducts := info.Item.Products
userAccounts := info.Accounts
color.Yellow("\n[i] Item ID: %s", info.Item.ItemID)
color.Yellow("\n[i] Accounts Info:")
t1 := table.NewWriter()
t1.SetOutputMirror(os.Stdout)
t1.AppendHeader(table.Row{"ID", "Name", "Official Name", "Type", "Subtype"})
for _, account := range userAccounts {
t1.AppendRow(table.Row{
color.GreenString(account.AccountID),
color.GreenString(account.Name),
color.GreenString(account.OfficialName),
color.GreenString(account.Type),
color.GreenString(account.Subtype),
})
t1.AppendSeparator()
}
t1.SetOutputMirror(os.Stdout)
t1.Render()
color.Yellow("\n[i] Products:")
t2 := table.NewWriter()
t2.AppendHeader(table.Row{"Product Name", "Access Level", "Capabilities"})
for _, product := range plaidProducts {
productCell := color.GreenString(product.DisplayName)
productDescCell := color.GreenString(product.Description)
productPermissionCell := color.GreenString("Denied")
for _, productName := range userProducts {
if productName == product.Name {
permissionLevel := PermissionStrings[product.PermissionLevel]
productPermissionCell = "Granted" // If permission level is not defined, default to "Granted"
if len(permissionLevel) > 0 {
// Capitalize the perssion level string
capitalizedLevel := strings.ToUpper(string(permissionLevel[0])) + strings.ToLower(permissionLevel[1:])
productPermissionCell = color.GreenString(capitalizedLevel)
}
break
}
}
t2.AppendRow(table.Row{productCell, productPermissionCell, productDescCell})
t2.AppendSeparator()
}
t2.SetOutputMirror(os.Stdout)
t2.Render()
fmt.Printf("%s: https://plaid.com/docs/api/\n\n", color.GreenString("Ref"))
}
================================================
FILE: pkg/analyzer/analyzers/plaid/plaid_test.go
================================================
package plaid
import (
_ "embed"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*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("PLAIDKEY_SECRET")
clientID := testSecrets.MustGetField("PLAIDKEY_CLIENTID")
accessToken := testSecrets.MustGetField("PLAIDKEY_ACCESS_TOKEN")
tests := []struct {
name string
clientID string
secret string
accessToken string
want string
wantErr bool
}{
{
name: "valid plaid credentials",
clientID: clientID,
secret: secret,
accessToken: accessToken,
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{
"secret": tt.secret,
"id": tt.clientID,
"token": tt.accessToken,
})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
fmt.Println(string(gotJSON))
// compare the JSON strings
if string(gotJSON) != string(tt.want) {
// pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(tt.want, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/plaid/products.go
================================================
package plaid
type plaidProduct struct {
Name string
DisplayName string
Description string
PermissionLevel Permission
}
type Product int
const (
Assets Product = iota
Auth
Balance
BalancePlus
Beacon
CraBaseReport
CraIncomeInsights
CraPartnerInsights
CraNetworkInsights
CraCashflowInsights
CreditDetails
Employment
Identity
IdentityMatch
IdentityVerification
Income
IncomeVerification
Investments
InvestmentsAuth
Layer
Liabilities
PayByBank
PaymentInitiation
ProcessorPayments
ProcessorIdentity
Profile
RecurringTransactions
Signal
StandingOrders
Statements
Transactions
TransactionsRefresh
Transfer
)
var plaidProducts = map[Product]plaidProduct{
Assets: {
Name: "assets",
DisplayName: "Assets",
Description: "Request, retrieve and share detailed reports of financial assets and account history",
PermissionLevel: Write,
},
Auth: {
Name: "auth",
DisplayName: "Auth",
Description: "Retrieve account and routing numbers",
PermissionLevel: Read,
},
Balance: {
Name: "balance",
DisplayName: "Balance",
Description: "Check current and available account balance in real time",
PermissionLevel: Read,
},
BalancePlus: {
Name: "balance_plus",
DisplayName: "Balance Plus",
Description: "Estimate projected balances and financial runway",
PermissionLevel: Read,
},
Beacon: {
Name: "beacon",
DisplayName: "Beacon",
Description: "Generate risk insights and fraud signals based on user account behavior",
PermissionLevel: Write,
},
CraBaseReport: {
Name: "cra_base_report",
DisplayName: "CRA Base Report",
Description: "Generate a standardized financial report",
PermissionLevel: Write,
},
CraIncomeInsights: {
Name: "cra_income_insights",
DisplayName: "CRA Income Insights",
Description: "Analyze income trends and consistency",
PermissionLevel: Write,
},
CraPartnerInsights: {
Name: "cra_partner_insights",
DisplayName: "CRA Partner Insights",
Description: "Access custom insights",
PermissionLevel: Write,
},
CraNetworkInsights: {
Name: "cra_network_insights",
DisplayName: "CRA Network Insights",
Description: "View analytics and performance benchmarks",
PermissionLevel: Write,
},
CraCashflowInsights: {
Name: "cra_cashflow_insights",
DisplayName: "CRA Cashflow Insights",
Description: "Evaluate cash flow behavior including recurring income and expenses",
PermissionLevel: Write,
},
CreditDetails: {
Name: "credit_details",
DisplayName: "Credit Details",
Description: "Access credit account usage, limits, and repayment history",
PermissionLevel: Read,
},
Employment: {
Name: "employment",
DisplayName: "Employment",
Description: "Retrieve current employment status and employer details",
PermissionLevel: Read,
},
Identity: {
Name: "identity",
DisplayName: "Identity",
Description: "Access personal identity information like name, phone, address, and email",
PermissionLevel: Read,
},
IdentityMatch: {
Name: "identity_match",
DisplayName: "Identity Match",
Description: "Match user-provided identity details against institution records",
PermissionLevel: Read,
},
IdentityVerification: {
Name: "identity_verification",
DisplayName: "Identity Verification",
Description: "Verify user identity through government documents and identity data sources",
PermissionLevel: Write,
},
Income: {
Name: "income",
DisplayName: "Income",
Description: "Analyze income patterns based on transaction history",
PermissionLevel: Write,
},
IncomeVerification: {
Name: "income_verification",
DisplayName: "Income Verification",
Description: "Verify income through paystubs, payroll data, or bank information",
PermissionLevel: Write,
},
Investments: {
Name: "investments",
DisplayName: "Investments",
Description: "Retrieve holdings, balances, and historical investment transactions",
PermissionLevel: Read,
},
InvestmentsAuth: {
Name: "investments_auth",
DisplayName: "Investments Auth",
Description: "Retrieve account and routing numbers for investment accounts",
PermissionLevel: Read,
},
Layer: {
Name: "layer",
DisplayName: "Layer",
Description: "Use a simplified onboarding experience for linking financial accounts",
PermissionLevel: Read,
},
Liabilities: {
Name: "liabilities",
DisplayName: "Liabilities",
Description: "Access detailed information about loans, credit cards, and other liabilities",
PermissionLevel: Write,
},
PayByBank: {
Name: "pay_by_bank",
DisplayName: "Pay By Bank",
Description: "Initiate payments directly from the user's bank account",
PermissionLevel: Write,
},
PaymentInitiation: {
Name: "payment_initiation",
DisplayName: "Payment Initiation",
Description: "Create and manage payment requests and track their status",
PermissionLevel: Write,
},
ProcessorPayments: {
Name: "processor_payments",
DisplayName: "Processor Payments",
Description: "Send payment details securely to third-party processors",
PermissionLevel: Write,
},
ProcessorIdentity: {
Name: "processor_identity",
DisplayName: "Processor Identity",
Description: "Share identity data with payment processors for verification",
PermissionLevel: Read,
},
Profile: {
Name: "profile",
DisplayName: "Profile",
Description: "Access user profile data",
PermissionLevel: Read,
},
RecurringTransactions: {
Name: "recurring_transactions",
DisplayName: "Recurring Transactions",
Description: "Identify and analyze recurring payments and subscriptions",
PermissionLevel: Write,
},
Signal: {
Name: "signal",
DisplayName: "Signal",
Description: "Assess the likelihood of ACH returns",
PermissionLevel: Read,
},
StandingOrders: {
Name: "standing_orders",
DisplayName: "Standing Orders",
Description: "View and manage recurring scheduled bank transfers",
PermissionLevel: Write,
},
Statements: {
Name: "statements",
DisplayName: "Statements",
Description: "List and download historical bank statements in PDF format",
PermissionLevel: Read,
},
Transactions: {
Name: "transactions",
DisplayName: "Transactions",
Description: "Retrieve, filter, and analyze categorized transaction history",
PermissionLevel: Read,
},
TransactionsRefresh: {
Name: "transactions_refresh",
DisplayName: "Transactions Refresh",
Description: "Trigger a manual refresh to retrieve the latest transactions",
PermissionLevel: Read,
},
Transfer: {
Name: "transfer",
DisplayName: "Transfer",
Description: "Initiate, manage, and track bank transfers",
PermissionLevel: Write,
},
}
func GetProductByName(name string) (plaidProduct, bool) {
for _, product := range plaidProducts {
if product.Name == name {
return product, true
}
}
return plaidProduct{}, false
}
================================================
FILE: pkg/analyzer/analyzers/planetscale/expected_output.json
================================================
{"AnalyzerType":27,"Bindings":[{"Resource":{"Name":"detector-db","FullyQualifiedName":"planetscale.com/database/9p2lzxigxod0","Type":"Database","Metadata":null,"Parent":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null}},"Permission":{"Value":"connect_branch","Parent":null}},{"Resource":{"Name":"detector-db","FullyQualifiedName":"planetscale.com/database/9p2lzxigxod0","Type":"Database","Metadata":null,"Parent":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null}},"Permission":{"Value":"connect_production_branch","Parent":null}},{"Resource":{"Name":"detector-db","FullyQualifiedName":"planetscale.com/database/9p2lzxigxod0","Type":"Database","Metadata":null,"Parent":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null}},"Permission":{"Value":"create_branch","Parent":null}},{"Resource":{"Name":"detector-db","FullyQualifiedName":"planetscale.com/database/9p2lzxigxod0","Type":"Database","Metadata":null,"Parent":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null}},"Permission":{"Value":"create_deploy_request","Parent":null}},{"Resource":{"Name":"detector-db","FullyQualifiedName":"planetscale.com/database/9p2lzxigxod0","Type":"Database","Metadata":null,"Parent":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null}},"Permission":{"Value":"read_backups","Parent":null}},{"Resource":{"Name":"detector-db","FullyQualifiedName":"planetscale.com/database/9p2lzxigxod0","Type":"Database","Metadata":null,"Parent":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null}},"Permission":{"Value":"read_branch","Parent":null}},{"Resource":{"Name":"detector-db","FullyQualifiedName":"planetscale.com/database/9p2lzxigxod0","Type":"Database","Metadata":null,"Parent":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null}},"Permission":{"Value":"read_database","Parent":null}},{"Resource":{"Name":"detector-db","FullyQualifiedName":"planetscale.com/database/9p2lzxigxod0","Type":"Database","Metadata":null,"Parent":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null}},"Permission":{"Value":"read_deploy_request","Parent":null}},{"Resource":{"Name":"detector-db","FullyQualifiedName":"planetscale.com/database/9p2lzxigxod0","Type":"Database","Metadata":null,"Parent":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null}},"Permission":{"Value":"restore_backup","Parent":null}},{"Resource":{"Name":"detector-db","FullyQualifiedName":"planetscale.com/database/9p2lzxigxod0","Type":"Database","Metadata":null,"Parent":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null}},"Permission":{"Value":"restore_production_branch_backup","Parent":null}},{"Resource":{"Name":"detector-db","FullyQualifiedName":"planetscale.com/database/9p2lzxigxod0","Type":"Database","Metadata":null,"Parent":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null}},"Permission":{"Value":"write_database","Parent":null}},{"Resource":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null},"Permission":{"Value":"create_databases","Parent":null}},{"Resource":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null},"Permission":{"Value":"read_audit_logs","Parent":null}},{"Resource":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null},"Permission":{"Value":"read_databases","Parent":null}},{"Resource":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null},"Permission":{"Value":"read_invoices","Parent":null}},{"Resource":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null},"Permission":{"Value":"read_oauth_applications","Parent":null}},{"Resource":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null},"Permission":{"Value":"read_oauth_tokens","Parent":null}},{"Resource":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null},"Permission":{"Value":"read_organization","Parent":null}},{"Resource":{"Name":"detectors","FullyQualifiedName":"planetscale.com/organization/hn31ztkm9u15","Type":"Organization","Metadata":null,"Parent":null},"Permission":{"Value":"write_oauth_tokens","Parent":null}}],"UnboundedResources":null,"Metadata":null}
================================================
FILE: pkg/analyzer/analyzers/planetscale/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package planetscale
import "errors"
type Permission int
const (
Invalid Permission = iota
ReadOrganization Permission = iota
ReadInvoices Permission = iota
ReadDatabases Permission = iota
ReadAuditLogs Permission = iota
CreateDatabases Permission = iota
DeleteDatabases Permission = iota
ReadOauthApplications Permission = iota
WriteOauthTokens Permission = iota
ReadOauthTokens Permission = iota
DeleteOauthTokens Permission = iota
ReadDatabase Permission = iota
WriteDatabase Permission = iota
DeleteDatabase Permission = iota
ReadBranch Permission = iota
CreateBranch Permission = iota
DeleteBranch Permission = iota
DeleteBranchPassword Permission = iota
DeleteProductionBranch Permission = iota
DeleteProductionBranchPassword Permission = iota
ReadDeployRequest Permission = iota
CreateDeployRequest Permission = iota
ApproveDeployRequest Permission = iota
ConnectBranch Permission = iota
ConnectProductionBranch Permission = iota
ReadComment Permission = iota
CreateComment Permission = iota
RestoreBackup Permission = iota
WriteBackups Permission = iota
ReadBackups Permission = iota
DeleteBackups Permission = iota
RestoreProductionBranchBackup Permission = iota
DeleteProductionBranchBackups Permission = iota
)
var (
PermissionStrings = map[Permission]string{
ReadOrganization: "read_organization",
ReadInvoices: "read_invoices",
ReadDatabases: "read_databases",
ReadAuditLogs: "read_audit_logs",
CreateDatabases: "create_databases",
DeleteDatabases: "delete_databases",
ReadOauthApplications: "read_oauth_applications",
WriteOauthTokens: "write_oauth_tokens",
ReadOauthTokens: "read_oauth_tokens",
DeleteOauthTokens: "delete_oauth_tokens",
ReadDatabase: "read_database",
WriteDatabase: "write_database",
DeleteDatabase: "delete_database",
ReadBranch: "read_branch",
CreateBranch: "create_branch",
DeleteBranch: "delete_branch",
DeleteBranchPassword: "delete_branch_password",
DeleteProductionBranch: "delete_production_branch",
DeleteProductionBranchPassword: "delete_production_branch_password",
ReadDeployRequest: "read_deploy_request",
CreateDeployRequest: "create_deploy_request",
ApproveDeployRequest: "approve_deploy_request",
ConnectBranch: "connect_branch",
ConnectProductionBranch: "connect_production_branch",
ReadComment: "read_comment",
CreateComment: "create_comment",
RestoreBackup: "restore_backup",
WriteBackups: "write_backups",
ReadBackups: "read_backups",
DeleteBackups: "delete_backups",
RestoreProductionBranchBackup: "restore_production_branch_backup",
DeleteProductionBranchBackups: "delete_production_branch_backups",
}
StringToPermission = map[string]Permission{
"read_organization": ReadOrganization,
"read_invoices": ReadInvoices,
"read_databases": ReadDatabases,
"read_audit_logs": ReadAuditLogs,
"create_databases": CreateDatabases,
"delete_databases": DeleteDatabases,
"read_oauth_applications": ReadOauthApplications,
"write_oauth_tokens": WriteOauthTokens,
"read_oauth_tokens": ReadOauthTokens,
"delete_oauth_tokens": DeleteOauthTokens,
"read_database": ReadDatabase,
"write_database": WriteDatabase,
"delete_database": DeleteDatabase,
"read_branch": ReadBranch,
"create_branch": CreateBranch,
"delete_branch": DeleteBranch,
"delete_branch_password": DeleteBranchPassword,
"delete_production_branch": DeleteProductionBranch,
"delete_production_branch_password": DeleteProductionBranchPassword,
"read_deploy_request": ReadDeployRequest,
"create_deploy_request": CreateDeployRequest,
"approve_deploy_request": ApproveDeployRequest,
"connect_branch": ConnectBranch,
"connect_production_branch": ConnectProductionBranch,
"read_comment": ReadComment,
"create_comment": CreateComment,
"restore_backup": RestoreBackup,
"write_backups": WriteBackups,
"read_backups": ReadBackups,
"delete_backups": DeleteBackups,
"restore_production_branch_backup": RestoreProductionBranchBackup,
"delete_production_branch_backups": DeleteProductionBranchBackups,
}
PermissionIDs = map[Permission]int{
ReadOrganization: 1,
ReadInvoices: 2,
ReadDatabases: 3,
ReadAuditLogs: 4,
CreateDatabases: 5,
DeleteDatabases: 6,
ReadOauthApplications: 7,
WriteOauthTokens: 8,
ReadOauthTokens: 9,
DeleteOauthTokens: 10,
ReadDatabase: 11,
WriteDatabase: 12,
DeleteDatabase: 13,
ReadBranch: 14,
CreateBranch: 15,
DeleteBranch: 16,
DeleteBranchPassword: 17,
DeleteProductionBranch: 18,
DeleteProductionBranchPassword: 19,
ReadDeployRequest: 20,
CreateDeployRequest: 21,
ApproveDeployRequest: 22,
ConnectBranch: 23,
ConnectProductionBranch: 24,
ReadComment: 25,
CreateComment: 26,
RestoreBackup: 27,
WriteBackups: 28,
ReadBackups: 29,
DeleteBackups: 30,
RestoreProductionBranchBackup: 31,
DeleteProductionBranchBackups: 32,
}
IdToPermission = map[int]Permission{
1: ReadOrganization,
2: ReadInvoices,
3: ReadDatabases,
4: ReadAuditLogs,
5: CreateDatabases,
6: DeleteDatabases,
7: ReadOauthApplications,
8: WriteOauthTokens,
9: ReadOauthTokens,
10: DeleteOauthTokens,
11: ReadDatabase,
12: WriteDatabase,
13: DeleteDatabase,
14: ReadBranch,
15: CreateBranch,
16: DeleteBranch,
17: DeleteBranchPassword,
18: DeleteProductionBranch,
19: DeleteProductionBranchPassword,
20: ReadDeployRequest,
21: CreateDeployRequest,
22: ApproveDeployRequest,
23: ConnectBranch,
24: ConnectProductionBranch,
25: ReadComment,
26: CreateComment,
27: RestoreBackup,
28: WriteBackups,
29: ReadBackups,
30: DeleteBackups,
31: RestoreProductionBranchBackup,
32: DeleteProductionBranchBackups,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/planetscale/permissions.yaml
================================================
permissions:
- read_organization
- read_invoices
- read_databases
- read_audit_logs
- create_databases
- delete_databases
- read_oauth_applications
- write_oauth_tokens
- read_oauth_tokens
- delete_oauth_tokens
- read_database
- write_database
- delete_database
- read_branch
- create_branch
- delete_branch
- delete_branch_password
- delete_production_branch
- delete_production_branch_password
- read_deploy_request
- create_deploy_request
- approve_deploy_request
- connect_branch
- connect_production_branch
- read_comment
- create_comment
- restore_backup
- write_backups
- read_backups
- delete_backups
- restore_production_branch_backup
- delete_production_branch_backups
================================================
FILE: pkg/analyzer/analyzers/planetscale/planetscale.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go planetscale
package planetscale
import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypePlanetScale }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
id, ok := credInfo["id"]
if !ok {
return nil, errors.New("missing id in credInfo")
}
key, ok := credInfo["token"]
if !ok {
return nil, errors.New("missing key in credInfo")
}
info, err := AnalyzePermissions(a.Cfg, id, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypePlanetScale,
Metadata: nil,
Bindings: make([]analyzers.Binding, 0),
}
resource := analyzers.Resource{
Name: info.Organization.Name,
FullyQualifiedName: "planetscale.com/organization/" + info.Organization.Id,
Type: "Organization",
}
for _, permission := range info.OrgPermissions {
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: resource,
Permission: analyzers.Permission{
Value: permission,
},
})
}
for db, permissions := range info.DBPermissions {
dbResource := analyzers.Resource{
Name: db.Name,
FullyQualifiedName: "planetscale.com/database/" + db.Id,
Type: "Database",
Parent: &resource,
}
for _, permission := range permissions {
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: dbResource,
Permission: analyzers.Permission{
Value: permission,
},
})
}
}
return &result
}
//go:embed scopes.json
var scopesConfig []byte
type HttpStatusTest struct {
Endpoint string `json:"endpoint"`
Method string `json:"method"`
Payload interface{} `json:"payload"`
ValidStatuses []int `json:"valid_status_code"`
InvalidStatuses []int `json:"invalid_status_code"`
}
func StatusContains(status int, vals []int) bool {
for _, v := range vals {
if status == v {
return true
}
}
return false
}
func (h *HttpStatusTest) RunTest(cfg *config.Config, headers map[string]string, args ...any) (bool, error) {
// If body data, marshal to JSON
var data io.Reader
if h.Payload != nil {
jsonData, err := json.Marshal(h.Payload)
if err != nil {
return false, err
}
data = bytes.NewBuffer(jsonData)
}
// Create new HTTP request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest(h.Method, fmt.Sprintf(h.Endpoint, args...), data)
if err != nil {
return false, err
}
// Add custom headers if provided
for key, value := range headers {
req.Header.Set(key, value)
}
req.Header.Add("Content-Type", "application/json")
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// Check response status code
switch {
case StatusContains(resp.StatusCode, h.ValidStatuses):
return true, nil
case StatusContains(resp.StatusCode, h.InvalidStatuses):
return false, nil
default:
return false, errors.New("error checking response status code")
}
}
type Scopes struct {
OrganizationScopes []Scope `json:"organization_scopes"`
OAuthApplicationScopes []Scope `json:"oauth_application_scopes"`
DatabaseScopes []Scope `json:"database_scopes"`
DeployRequestScopes []Scope `json:"deploy_request_scopes"`
BranchScopes []BranchScope `json:"branch_scopes"`
BackupScopes []BranchScope `json:"backup_scopes"`
}
type Scope struct {
Name string `json:"name"`
HttpTest HttpStatusTest `json:"test"`
}
type BranchScope struct {
Scope
Production bool `json:"production"`
}
func readInScopes() (*Scopes, error) {
var scopes Scopes
if err := json.Unmarshal(scopesConfig, &scopes); err != nil {
return nil, err
}
return &scopes, nil
}
func checkPermissions(cfg *config.Config, scopes []Scope, id, key string, args ...any) ([]string, error) {
permissions := make([]string, 0)
for _, scope := range scopes {
status, err := scope.HttpTest.RunTest(cfg, map[string]string{"Authorization": fmt.Sprintf("%s:%s", id, key)}, args...)
if err != nil {
return nil, fmt.Errorf("running test: %w", err)
}
if status {
permissions = append(permissions, scope.Name)
}
}
return permissions, nil
}
func checkBranchPermissions(cfg *config.Config, scopes []BranchScope, id, key, organization, db, branch string, production bool) ([]string, error) {
permissions := make([]string, 0)
for _, scope := range scopes {
// check if scope is for production or non production branch
if production != scope.Production {
continue
}
status, err := scope.HttpTest.RunTest(cfg, map[string]string{"Authorization": fmt.Sprintf("%s:%s", id, key)}, organization, db, branch)
if err != nil {
return nil, fmt.Errorf("running test: %w", err)
}
if status {
permissions = append(permissions, scope.Name)
}
}
return permissions, nil
}
func checkBackupPermissions(cfg *config.Config, scopes []BranchScope, id, key, organization, db, backupId string, production bool) ([]string, error) {
permissions := make([]string, 0)
for _, scope := range scopes {
// check if scope is for production or non production branch
if production != scope.Production {
continue
}
scope.HttpTest.Payload = map[string]string{"backup_id": backupId}
status, err := scope.HttpTest.RunTest(cfg, map[string]string{"Authorization": fmt.Sprintf("%s:%s", id, key)}, organization, db)
if err != nil {
return nil, fmt.Errorf("running test: %w", err)
}
if status {
permissions = append(permissions, scope.Name)
}
}
return permissions, nil
}
type SecretInfo struct {
Organization organization
OrgPermissions []string
DBPermissions map[Database][]string
UnverifiedPermissions []string
}
func AnalyzeAndPrintPermissions(cfg *config.Config, id, token string) {
info, err := AnalyzePermissions(cfg, id, token)
if err != nil {
color.Red("[x] Error : %s", err.Error())
return
}
color.Green("[!] Valid PlanetScale credentials\n\n")
color.Green("[i] Organization: %s\n\n", info.Organization.Name)
printOrganizationPermissions(info.OrgPermissions)
if len(info.DBPermissions) > 0 {
printDatabasePermissions(info.DBPermissions)
}
printUnverifiedPermissions(info.UnverifiedPermissions)
}
func AnalyzePermissions(cfg *config.Config, id, token string) (*SecretInfo, error) {
var info = &SecretInfo{}
org, err := getOrganization(cfg, id, token)
if err != nil {
return nil, err
}
info.Organization = *org
scopes, err := readInScopes()
if err != nil {
return nil, fmt.Errorf("reading in scopes: %w", err)
}
// organization permissions
orgPermissions, err := getOrganizationPermissions(cfg, scopes, id, token, org.Name)
if err != nil {
return nil, err
}
info.OrgPermissions = orgPermissions
// database permissions
dbPermissions, err := getDatabasePermissions(cfg, scopes, id, token, org.Name)
if err != nil {
return nil, err
}
info.DBPermissions = dbPermissions
// These are permissions that can not be verified,
// either due to no endpoint available that specifically requires the permission
// or there does not exist a way to verify these permissions without changing the state of the system (mostly DELETE permissions)
info.UnverifiedPermissions = []string{
PermissionStrings[ReadComment],
PermissionStrings[CreateComment],
PermissionStrings[ApproveDeployRequest],
PermissionStrings[DeleteDatabases],
PermissionStrings[DeleteDatabase],
PermissionStrings[DeleteOauthTokens],
PermissionStrings[DeleteBranch],
PermissionStrings[DeleteBranchPassword],
PermissionStrings[DeleteProductionBranch],
PermissionStrings[DeleteProductionBranchPassword],
PermissionStrings[DeleteBackups],
PermissionStrings[DeleteProductionBranchBackups],
PermissionStrings[WriteBackups],
}
return info, nil
}
type organization struct {
Id string `json:"id"`
Name string `json:"name"`
}
type organizationJSON struct {
Data []organization `json:"data"`
}
func getOrganization(cfg *config.Config, id, key string) (*organization, error) {
url := "https://api.planetscale.com/v1/organizations"
var organizationJSON organizationJSON
err := sendGetRequest(cfg, id, key, url, &organizationJSON)
if err != nil {
return nil, err
}
if len(organizationJSON.Data) == 0 {
return nil, errors.New("invalid api credentials")
}
return &organizationJSON.Data[0], nil
}
func getOrganizationPermissions(cfg *config.Config, scopes *Scopes, id, token, orgName string) ([]string, error) {
organizationPermissions, err := checkPermissions(cfg, scopes.OrganizationScopes, id, token, orgName)
if err != nil {
return nil, err
}
oauthPermissions, err := getOAuthApplicationPermissions(cfg, scopes.OAuthApplicationScopes, id, token, orgName)
if err != nil {
return nil, err
}
organizationPermissions = append(organizationPermissions, oauthPermissions...)
return organizationPermissions, nil
}
func getOAuthApplicationPermissions(cfg *config.Config, scopes []Scope, id, key, organization string) ([]string, error) {
oauthApplicationId, err := getOAuthApplicationId(cfg, id, key, organization)
if err != nil {
return nil, err
}
if oauthApplicationId != "" {
oauthPermissions, err := checkPermissions(cfg, scopes, id, key, organization, oauthApplicationId)
if err != nil {
return nil, err
}
return oauthPermissions, nil
}
return nil, nil
}
type oauthApplicationJSON struct {
Data []struct {
Id string `json:"id"`
}
}
func getOAuthApplicationId(cfg *config.Config, id, key, organization string) (string, error) {
url := fmt.Sprintf("https://api.planetscale.com/v1/organizations/%s/oauth-applications", organization)
var oauthApplicationJSON oauthApplicationJSON
err := sendGetRequest(cfg, id, key, url, &oauthApplicationJSON)
if err != nil {
return "", err
}
if len(oauthApplicationJSON.Data) > 0 {
return oauthApplicationJSON.Data[0].Id, nil
}
return "", nil // no oauth application found
}
func getDatabasePermissions(cfg *config.Config, scopes *Scopes, id, token, orgName string) (map[Database][]string, error) {
databases, err := getDatabases(cfg, id, token, orgName)
if err != nil {
return nil, err
}
dbPermissionsMap := make(map[Database][]string)
for _, database := range databases {
dbPermissions, err := checkPermissions(cfg, scopes.DatabaseScopes, id, token, orgName, database.Name)
if err != nil {
return nil, err
}
dbPermissionsMap[database] = dbPermissions
branchPermissions, err := getBranchPermissions(cfg, scopes, id, token, orgName, database.Name)
if err != nil {
return nil, err
}
dbPermissionsMap[database] = append(dbPermissionsMap[database], branchPermissions...)
}
return dbPermissionsMap, nil
}
func getBranchPermissions(cfg *config.Config, scopes *Scopes, id, token, orgName, dbName string) ([]string, error) {
branches, err := getDbBranches(cfg, id, token, orgName, dbName)
if err != nil {
return nil, err
}
// get permissions for prod and non prod branches
prodDone, nonProdDone := false, false
allBranchPermissions := make([]string, 0)
for _, branch := range branches {
// check if we have already checked permissions for prod or non prod branches
if (prodDone && branch.Production) || (nonProdDone && !branch.Production) {
continue
}
if branch.Production {
prodDone = true
} else {
nonProdDone = true
}
branchPermissions, err := checkBranchPermissions(cfg, scopes.BranchScopes, id, token, orgName, dbName, branch.Name, branch.Production)
if err != nil {
return nil, err
}
allBranchPermissions = append(allBranchPermissions, branchPermissions...)
backupId, err := getBackupId(cfg, id, token, orgName, dbName, branch.Name)
if err != nil {
return nil, err
}
if backupId != "" {
backupPermissions, err := checkBackupPermissions(cfg, scopes.BackupScopes, id, token, orgName, dbName, backupId, branch.Production)
if err != nil {
return nil, err
}
allBranchPermissions = append(allBranchPermissions, backupPermissions...)
}
if prodDone && nonProdDone {
break
}
}
return allBranchPermissions, err
}
type Database struct {
Id string `json:"id"`
Name string `json:"name"`
}
type databasesJSON struct {
Data []Database `json:"data"`
NextPageUrl string `json:"next_page_url"`
}
func getDatabases(cfg *config.Config, id, key, organization string) ([]Database, error) {
url := fmt.Sprintf("https://api.planetscale.com/v1/organizations/%s/databases", organization)
databases := make([]Database, 0)
// loop for pagination
for url != "" {
var databasesResponse databasesJSON
err := sendGetRequest(cfg, id, key, url, &databasesResponse)
if err != nil {
return nil, err
}
databases = append(databases, databasesResponse.Data...)
url = databasesResponse.NextPageUrl
}
return databases, nil
}
type Branch struct {
Id string `json:"id"`
Name string `json:"name"`
Production bool `json:"production"`
}
type branchesJSON struct {
Data []Branch `json:"data"`
}
func getDbBranches(cfg *config.Config, id, key, organization, db string) ([]Branch, error) {
url := fmt.Sprintf("https://api.planetscale.com/v1/organizations/%s/databases/%s/branches", organization, db)
var branchesResponse branchesJSON
err := sendGetRequest(cfg, id, key, url, &branchesResponse)
if err != nil {
return nil, err
}
return branchesResponse.Data, nil
}
type backupsJson struct {
Data []struct {
Id string `json:"id"`
}
}
func getBackupId(cfg *config.Config, id, key, organization, db, branch string) (string, error) {
url := fmt.Sprintf("https://api.planetscale.com/v1/organizations/%s/databases/%s/branches/%s/backups", organization, db, branch)
var backupsResponse backupsJson
err := sendGetRequest(cfg, id, key, url, &backupsResponse)
if err != nil {
return "", err
}
if len(backupsResponse.Data) > 0 {
return backupsResponse.Data[0].Id, nil
}
return "", nil // no backups found
}
func sendGetRequest(cfg *config.Config, id, key, url string, responseObj interface{}) error {
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", fmt.Sprintf("%s:%s", id, key))
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Check response status code
switch resp.StatusCode {
case http.StatusOK:
// Decode response body
err = json.NewDecoder(resp.Body).Decode(&responseObj)
if err != nil {
return err
}
return nil // response successfully decoded
case http.StatusForbidden:
return nil // no permission
default:
return fmt.Errorf("unexpected status code %d", resp.StatusCode)
}
}
func printOrganizationPermissions(permissions []string) {
color.Yellow("[i] Organization Permissions:")
if len(permissions) == 0 {
color.Yellow("No permissions found")
} else {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Permission"})
for _, permission := range permissions {
t.AppendRow(table.Row{color.GreenString(permission)})
}
t.Render()
}
}
func printDatabasePermissions(permissions map[Database][]string) {
color.Yellow("[i] Database Permissions:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Database", "Permission"})
for database, dbPermissions := range permissions {
t.AppendRow(table.Row{database.Name, color.GreenString(strings.Join(dbPermissions, ", "))})
}
t.Render()
}
func printUnverifiedPermissions(permissions []string) {
color.Yellow("[i] Unverified Permissions:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Permission"})
for _, permission := range permissions {
t.AppendRow(table.Row{color.YellowString(permission)})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/planetscale/planetscale_test.go
================================================
package planetscale
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
id string
token string
want string // JSON string
wantErr bool
}{
{
name: "valid planetscale id and key",
id: testSecrets.MustGetField("PLANET_SCALE_ID"),
token: testSecrets.MustGetField("PLANET_SCALE_TOKEN"),
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"id": tt.id, "token": tt.token})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/planetscale/scopes.json
================================================
{
"organization_scopes": [
{
"name": "read_organization",
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "read_invoices",
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/invoices",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "read_databases",
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/databases",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "read_audit_logs",
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/audit-log",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "create_databases",
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/databases",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "read_oauth_applications",
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/oauth-applications",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
}
],
"oauth_application_scopes": [
{
"name": "write_oauth_tokens",
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/oauth-applications/%s/token",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "read_oauth_tokens",
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/oauth-applications/%s/tokens",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
}
],
"database_scopes": [
{
"name": "read_database",
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/databases/%s",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "write_database",
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/databases/%s",
"method": "PATCH",
"valid_status_code": [400],
"invalid_status_code": [403],
"payload": {
"default_branch": "`nowaythisbranchcanexist"
}
}
},
{
"name": "read_branch",
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/databases/%s/branches",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "create_branch",
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/databases/%s/branches",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
},
{
"name": "read_deploy_request",
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/databases/%s/deploy-requests",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "create_deploy_request",
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/databases/%s/deploy-requests",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403]
}
}
],
"branch_scopes": [
{
"name": "connect_branch",
"production": false,
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/databases/%s/branches/%s/passwords",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403],
"payload": {
"role": "`nowaythisrolecanexist"
}
}
},
{
"name": "connect_production_branch",
"production": true,
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/databases/%s/branches/%s/passwords",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403],
"payload": {
"role": "`nowaythisrolecanexist"
}
}
},
{
"name": "read_backups",
"production": true,
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/databases/%s/branches/%s/backups",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
}
],
"backup_scopes": [
{
"name": "restore_backup",
"production": false,
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/databases/%s/branches",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403],
"payload": {
"backup_id": "%s"
}
}
},
{
"name": "restore_production_branch_backup",
"production": true,
"test": {
"endpoint": "https://api.planetscale.com/v1/organizations/%s/databases/%s/branches",
"method": "POST",
"valid_status_code": [422],
"invalid_status_code": [403],
"payload": {
"backup_id": "%s"
}
}
}
]
}
================================================
FILE: pkg/analyzer/analyzers/postgres/expected_output.json
================================================
{"AnalyzerType":12,"Bindings":[{"Resource":{"Name":"_pg_foreign_data_wrappers","FullyQualifiedName":"localhost/postgres/_pg_foreign_data_wrappers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"_pg_foreign_data_wrappers","FullyQualifiedName":"localhost/postgres/_pg_foreign_data_wrappers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"_pg_foreign_data_wrappers","FullyQualifiedName":"localhost/postgres/_pg_foreign_data_wrappers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"_pg_foreign_data_wrappers","FullyQualifiedName":"localhost/postgres/_pg_foreign_data_wrappers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"_pg_foreign_data_wrappers","FullyQualifiedName":"localhost/postgres/_pg_foreign_data_wrappers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"_pg_foreign_data_wrappers","FullyQualifiedName":"localhost/postgres/_pg_foreign_data_wrappers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"_pg_foreign_data_wrappers","FullyQualifiedName":"localhost/postgres/_pg_foreign_data_wrappers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"_pg_foreign_servers","FullyQualifiedName":"localhost/postgres/_pg_foreign_servers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"_pg_foreign_servers","FullyQualifiedName":"localhost/postgres/_pg_foreign_servers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"_pg_foreign_servers","FullyQualifiedName":"localhost/postgres/_pg_foreign_servers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"_pg_foreign_servers","FullyQualifiedName":"localhost/postgres/_pg_foreign_servers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"_pg_foreign_servers","FullyQualifiedName":"localhost/postgres/_pg_foreign_servers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"_pg_foreign_servers","FullyQualifiedName":"localhost/postgres/_pg_foreign_servers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"_pg_foreign_servers","FullyQualifiedName":"localhost/postgres/_pg_foreign_servers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"_pg_foreign_table_columns","FullyQualifiedName":"localhost/postgres/_pg_foreign_table_columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"_pg_foreign_table_columns","FullyQualifiedName":"localhost/postgres/_pg_foreign_table_columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"_pg_foreign_table_columns","FullyQualifiedName":"localhost/postgres/_pg_foreign_table_columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"_pg_foreign_table_columns","FullyQualifiedName":"localhost/postgres/_pg_foreign_table_columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"_pg_foreign_table_columns","FullyQualifiedName":"localhost/postgres/_pg_foreign_table_columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"_pg_foreign_table_columns","FullyQualifiedName":"localhost/postgres/_pg_foreign_table_columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"_pg_foreign_table_columns","FullyQualifiedName":"localhost/postgres/_pg_foreign_table_columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"_pg_foreign_tables","FullyQualifiedName":"localhost/postgres/_pg_foreign_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"_pg_foreign_tables","FullyQualifiedName":"localhost/postgres/_pg_foreign_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"_pg_foreign_tables","FullyQualifiedName":"localhost/postgres/_pg_foreign_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"_pg_foreign_tables","FullyQualifiedName":"localhost/postgres/_pg_foreign_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"_pg_foreign_tables","FullyQualifiedName":"localhost/postgres/_pg_foreign_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"_pg_foreign_tables","FullyQualifiedName":"localhost/postgres/_pg_foreign_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"_pg_foreign_tables","FullyQualifiedName":"localhost/postgres/_pg_foreign_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"_pg_user_mappings","FullyQualifiedName":"localhost/postgres/_pg_user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"_pg_user_mappings","FullyQualifiedName":"localhost/postgres/_pg_user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"_pg_user_mappings","FullyQualifiedName":"localhost/postgres/_pg_user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"_pg_user_mappings","FullyQualifiedName":"localhost/postgres/_pg_user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"_pg_user_mappings","FullyQualifiedName":"localhost/postgres/_pg_user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"_pg_user_mappings","FullyQualifiedName":"localhost/postgres/_pg_user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"_pg_user_mappings","FullyQualifiedName":"localhost/postgres/_pg_user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"administrable_role_authorizations","FullyQualifiedName":"localhost/postgres/administrable_role_authorizations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"administrable_role_authorizations","FullyQualifiedName":"localhost/postgres/administrable_role_authorizations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"administrable_role_authorizations","FullyQualifiedName":"localhost/postgres/administrable_role_authorizations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"administrable_role_authorizations","FullyQualifiedName":"localhost/postgres/administrable_role_authorizations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"administrable_role_authorizations","FullyQualifiedName":"localhost/postgres/administrable_role_authorizations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"administrable_role_authorizations","FullyQualifiedName":"localhost/postgres/administrable_role_authorizations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"administrable_role_authorizations","FullyQualifiedName":"localhost/postgres/administrable_role_authorizations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"applicable_roles","FullyQualifiedName":"localhost/postgres/applicable_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"applicable_roles","FullyQualifiedName":"localhost/postgres/applicable_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"applicable_roles","FullyQualifiedName":"localhost/postgres/applicable_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"applicable_roles","FullyQualifiedName":"localhost/postgres/applicable_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"applicable_roles","FullyQualifiedName":"localhost/postgres/applicable_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"applicable_roles","FullyQualifiedName":"localhost/postgres/applicable_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"applicable_roles","FullyQualifiedName":"localhost/postgres/applicable_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"attributes","FullyQualifiedName":"localhost/postgres/attributes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"attributes","FullyQualifiedName":"localhost/postgres/attributes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"attributes","FullyQualifiedName":"localhost/postgres/attributes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"attributes","FullyQualifiedName":"localhost/postgres/attributes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"attributes","FullyQualifiedName":"localhost/postgres/attributes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"attributes","FullyQualifiedName":"localhost/postgres/attributes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"attributes","FullyQualifiedName":"localhost/postgres/attributes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"character_sets","FullyQualifiedName":"localhost/postgres/character_sets","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"character_sets","FullyQualifiedName":"localhost/postgres/character_sets","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"character_sets","FullyQualifiedName":"localhost/postgres/character_sets","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"character_sets","FullyQualifiedName":"localhost/postgres/character_sets","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"character_sets","FullyQualifiedName":"localhost/postgres/character_sets","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"character_sets","FullyQualifiedName":"localhost/postgres/character_sets","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"character_sets","FullyQualifiedName":"localhost/postgres/character_sets","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"check_constraint_routine_usage","FullyQualifiedName":"localhost/postgres/check_constraint_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"check_constraint_routine_usage","FullyQualifiedName":"localhost/postgres/check_constraint_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"check_constraint_routine_usage","FullyQualifiedName":"localhost/postgres/check_constraint_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"check_constraint_routine_usage","FullyQualifiedName":"localhost/postgres/check_constraint_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"check_constraint_routine_usage","FullyQualifiedName":"localhost/postgres/check_constraint_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"check_constraint_routine_usage","FullyQualifiedName":"localhost/postgres/check_constraint_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"check_constraint_routine_usage","FullyQualifiedName":"localhost/postgres/check_constraint_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"check_constraints","FullyQualifiedName":"localhost/postgres/check_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"check_constraints","FullyQualifiedName":"localhost/postgres/check_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"check_constraints","FullyQualifiedName":"localhost/postgres/check_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"check_constraints","FullyQualifiedName":"localhost/postgres/check_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"check_constraints","FullyQualifiedName":"localhost/postgres/check_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"check_constraints","FullyQualifiedName":"localhost/postgres/check_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"check_constraints","FullyQualifiedName":"localhost/postgres/check_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"collation_character_set_applicability","FullyQualifiedName":"localhost/postgres/collation_character_set_applicability","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"collation_character_set_applicability","FullyQualifiedName":"localhost/postgres/collation_character_set_applicability","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"collation_character_set_applicability","FullyQualifiedName":"localhost/postgres/collation_character_set_applicability","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"collation_character_set_applicability","FullyQualifiedName":"localhost/postgres/collation_character_set_applicability","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"collation_character_set_applicability","FullyQualifiedName":"localhost/postgres/collation_character_set_applicability","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"collation_character_set_applicability","FullyQualifiedName":"localhost/postgres/collation_character_set_applicability","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"collation_character_set_applicability","FullyQualifiedName":"localhost/postgres/collation_character_set_applicability","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"collations","FullyQualifiedName":"localhost/postgres/collations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"collations","FullyQualifiedName":"localhost/postgres/collations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"collations","FullyQualifiedName":"localhost/postgres/collations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"collations","FullyQualifiedName":"localhost/postgres/collations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"collations","FullyQualifiedName":"localhost/postgres/collations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"collations","FullyQualifiedName":"localhost/postgres/collations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"collations","FullyQualifiedName":"localhost/postgres/collations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"column_column_usage","FullyQualifiedName":"localhost/postgres/column_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"column_column_usage","FullyQualifiedName":"localhost/postgres/column_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"column_column_usage","FullyQualifiedName":"localhost/postgres/column_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"column_column_usage","FullyQualifiedName":"localhost/postgres/column_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"column_column_usage","FullyQualifiedName":"localhost/postgres/column_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"column_column_usage","FullyQualifiedName":"localhost/postgres/column_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"column_column_usage","FullyQualifiedName":"localhost/postgres/column_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"column_domain_usage","FullyQualifiedName":"localhost/postgres/column_domain_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"column_domain_usage","FullyQualifiedName":"localhost/postgres/column_domain_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"column_domain_usage","FullyQualifiedName":"localhost/postgres/column_domain_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"column_domain_usage","FullyQualifiedName":"localhost/postgres/column_domain_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"column_domain_usage","FullyQualifiedName":"localhost/postgres/column_domain_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"column_domain_usage","FullyQualifiedName":"localhost/postgres/column_domain_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"column_domain_usage","FullyQualifiedName":"localhost/postgres/column_domain_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"column_options","FullyQualifiedName":"localhost/postgres/column_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"column_options","FullyQualifiedName":"localhost/postgres/column_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"column_options","FullyQualifiedName":"localhost/postgres/column_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"column_options","FullyQualifiedName":"localhost/postgres/column_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"column_options","FullyQualifiedName":"localhost/postgres/column_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"column_options","FullyQualifiedName":"localhost/postgres/column_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"column_options","FullyQualifiedName":"localhost/postgres/column_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"column_privileges","FullyQualifiedName":"localhost/postgres/column_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"column_privileges","FullyQualifiedName":"localhost/postgres/column_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"column_privileges","FullyQualifiedName":"localhost/postgres/column_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"column_privileges","FullyQualifiedName":"localhost/postgres/column_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"column_privileges","FullyQualifiedName":"localhost/postgres/column_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"column_privileges","FullyQualifiedName":"localhost/postgres/column_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"column_privileges","FullyQualifiedName":"localhost/postgres/column_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"column_udt_usage","FullyQualifiedName":"localhost/postgres/column_udt_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"column_udt_usage","FullyQualifiedName":"localhost/postgres/column_udt_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"column_udt_usage","FullyQualifiedName":"localhost/postgres/column_udt_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"column_udt_usage","FullyQualifiedName":"localhost/postgres/column_udt_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"column_udt_usage","FullyQualifiedName":"localhost/postgres/column_udt_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"column_udt_usage","FullyQualifiedName":"localhost/postgres/column_udt_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"column_udt_usage","FullyQualifiedName":"localhost/postgres/column_udt_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"columns","FullyQualifiedName":"localhost/postgres/columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"columns","FullyQualifiedName":"localhost/postgres/columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"columns","FullyQualifiedName":"localhost/postgres/columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"columns","FullyQualifiedName":"localhost/postgres/columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"columns","FullyQualifiedName":"localhost/postgres/columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"columns","FullyQualifiedName":"localhost/postgres/columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"columns","FullyQualifiedName":"localhost/postgres/columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"constraint_column_usage","FullyQualifiedName":"localhost/postgres/constraint_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"constraint_column_usage","FullyQualifiedName":"localhost/postgres/constraint_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"constraint_column_usage","FullyQualifiedName":"localhost/postgres/constraint_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"constraint_column_usage","FullyQualifiedName":"localhost/postgres/constraint_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"constraint_column_usage","FullyQualifiedName":"localhost/postgres/constraint_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"constraint_column_usage","FullyQualifiedName":"localhost/postgres/constraint_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"constraint_column_usage","FullyQualifiedName":"localhost/postgres/constraint_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"constraint_table_usage","FullyQualifiedName":"localhost/postgres/constraint_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"constraint_table_usage","FullyQualifiedName":"localhost/postgres/constraint_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"constraint_table_usage","FullyQualifiedName":"localhost/postgres/constraint_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"constraint_table_usage","FullyQualifiedName":"localhost/postgres/constraint_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"constraint_table_usage","FullyQualifiedName":"localhost/postgres/constraint_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"constraint_table_usage","FullyQualifiedName":"localhost/postgres/constraint_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"constraint_table_usage","FullyQualifiedName":"localhost/postgres/constraint_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"data_type_privileges","FullyQualifiedName":"localhost/postgres/data_type_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"data_type_privileges","FullyQualifiedName":"localhost/postgres/data_type_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"data_type_privileges","FullyQualifiedName":"localhost/postgres/data_type_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"data_type_privileges","FullyQualifiedName":"localhost/postgres/data_type_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"data_type_privileges","FullyQualifiedName":"localhost/postgres/data_type_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"data_type_privileges","FullyQualifiedName":"localhost/postgres/data_type_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"data_type_privileges","FullyQualifiedName":"localhost/postgres/data_type_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"domain_constraints","FullyQualifiedName":"localhost/postgres/domain_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"domain_constraints","FullyQualifiedName":"localhost/postgres/domain_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"domain_constraints","FullyQualifiedName":"localhost/postgres/domain_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"domain_constraints","FullyQualifiedName":"localhost/postgres/domain_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"domain_constraints","FullyQualifiedName":"localhost/postgres/domain_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"domain_constraints","FullyQualifiedName":"localhost/postgres/domain_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"domain_constraints","FullyQualifiedName":"localhost/postgres/domain_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"domain_udt_usage","FullyQualifiedName":"localhost/postgres/domain_udt_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"domain_udt_usage","FullyQualifiedName":"localhost/postgres/domain_udt_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"domain_udt_usage","FullyQualifiedName":"localhost/postgres/domain_udt_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"domain_udt_usage","FullyQualifiedName":"localhost/postgres/domain_udt_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"domain_udt_usage","FullyQualifiedName":"localhost/postgres/domain_udt_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"domain_udt_usage","FullyQualifiedName":"localhost/postgres/domain_udt_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"domain_udt_usage","FullyQualifiedName":"localhost/postgres/domain_udt_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"domains","FullyQualifiedName":"localhost/postgres/domains","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"domains","FullyQualifiedName":"localhost/postgres/domains","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"domains","FullyQualifiedName":"localhost/postgres/domains","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"domains","FullyQualifiedName":"localhost/postgres/domains","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"domains","FullyQualifiedName":"localhost/postgres/domains","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"domains","FullyQualifiedName":"localhost/postgres/domains","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"domains","FullyQualifiedName":"localhost/postgres/domains","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"element_types","FullyQualifiedName":"localhost/postgres/element_types","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"element_types","FullyQualifiedName":"localhost/postgres/element_types","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"element_types","FullyQualifiedName":"localhost/postgres/element_types","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"element_types","FullyQualifiedName":"localhost/postgres/element_types","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"element_types","FullyQualifiedName":"localhost/postgres/element_types","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"element_types","FullyQualifiedName":"localhost/postgres/element_types","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"element_types","FullyQualifiedName":"localhost/postgres/element_types","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"enabled_roles","FullyQualifiedName":"localhost/postgres/enabled_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"enabled_roles","FullyQualifiedName":"localhost/postgres/enabled_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"enabled_roles","FullyQualifiedName":"localhost/postgres/enabled_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"enabled_roles","FullyQualifiedName":"localhost/postgres/enabled_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"enabled_roles","FullyQualifiedName":"localhost/postgres/enabled_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"enabled_roles","FullyQualifiedName":"localhost/postgres/enabled_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"enabled_roles","FullyQualifiedName":"localhost/postgres/enabled_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"foreign_data_wrapper_options","FullyQualifiedName":"localhost/postgres/foreign_data_wrapper_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"foreign_data_wrapper_options","FullyQualifiedName":"localhost/postgres/foreign_data_wrapper_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"foreign_data_wrapper_options","FullyQualifiedName":"localhost/postgres/foreign_data_wrapper_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"foreign_data_wrapper_options","FullyQualifiedName":"localhost/postgres/foreign_data_wrapper_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"foreign_data_wrapper_options","FullyQualifiedName":"localhost/postgres/foreign_data_wrapper_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"foreign_data_wrapper_options","FullyQualifiedName":"localhost/postgres/foreign_data_wrapper_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"foreign_data_wrapper_options","FullyQualifiedName":"localhost/postgres/foreign_data_wrapper_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"foreign_data_wrappers","FullyQualifiedName":"localhost/postgres/foreign_data_wrappers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"foreign_data_wrappers","FullyQualifiedName":"localhost/postgres/foreign_data_wrappers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"foreign_data_wrappers","FullyQualifiedName":"localhost/postgres/foreign_data_wrappers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"foreign_data_wrappers","FullyQualifiedName":"localhost/postgres/foreign_data_wrappers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"foreign_data_wrappers","FullyQualifiedName":"localhost/postgres/foreign_data_wrappers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"foreign_data_wrappers","FullyQualifiedName":"localhost/postgres/foreign_data_wrappers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"foreign_data_wrappers","FullyQualifiedName":"localhost/postgres/foreign_data_wrappers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"foreign_server_options","FullyQualifiedName":"localhost/postgres/foreign_server_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"foreign_server_options","FullyQualifiedName":"localhost/postgres/foreign_server_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"foreign_server_options","FullyQualifiedName":"localhost/postgres/foreign_server_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"foreign_server_options","FullyQualifiedName":"localhost/postgres/foreign_server_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"foreign_server_options","FullyQualifiedName":"localhost/postgres/foreign_server_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"foreign_server_options","FullyQualifiedName":"localhost/postgres/foreign_server_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"foreign_server_options","FullyQualifiedName":"localhost/postgres/foreign_server_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"foreign_servers","FullyQualifiedName":"localhost/postgres/foreign_servers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"foreign_servers","FullyQualifiedName":"localhost/postgres/foreign_servers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"foreign_servers","FullyQualifiedName":"localhost/postgres/foreign_servers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"foreign_servers","FullyQualifiedName":"localhost/postgres/foreign_servers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"foreign_servers","FullyQualifiedName":"localhost/postgres/foreign_servers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"foreign_servers","FullyQualifiedName":"localhost/postgres/foreign_servers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"foreign_servers","FullyQualifiedName":"localhost/postgres/foreign_servers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"foreign_table_options","FullyQualifiedName":"localhost/postgres/foreign_table_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"foreign_table_options","FullyQualifiedName":"localhost/postgres/foreign_table_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"foreign_table_options","FullyQualifiedName":"localhost/postgres/foreign_table_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"foreign_table_options","FullyQualifiedName":"localhost/postgres/foreign_table_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"foreign_table_options","FullyQualifiedName":"localhost/postgres/foreign_table_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"foreign_table_options","FullyQualifiedName":"localhost/postgres/foreign_table_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"foreign_table_options","FullyQualifiedName":"localhost/postgres/foreign_table_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"foreign_tables","FullyQualifiedName":"localhost/postgres/foreign_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"foreign_tables","FullyQualifiedName":"localhost/postgres/foreign_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"foreign_tables","FullyQualifiedName":"localhost/postgres/foreign_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"foreign_tables","FullyQualifiedName":"localhost/postgres/foreign_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"foreign_tables","FullyQualifiedName":"localhost/postgres/foreign_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"foreign_tables","FullyQualifiedName":"localhost/postgres/foreign_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"foreign_tables","FullyQualifiedName":"localhost/postgres/foreign_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"information_schema_catalog_name","FullyQualifiedName":"localhost/postgres/information_schema_catalog_name","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"information_schema_catalog_name","FullyQualifiedName":"localhost/postgres/information_schema_catalog_name","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"information_schema_catalog_name","FullyQualifiedName":"localhost/postgres/information_schema_catalog_name","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"information_schema_catalog_name","FullyQualifiedName":"localhost/postgres/information_schema_catalog_name","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"information_schema_catalog_name","FullyQualifiedName":"localhost/postgres/information_schema_catalog_name","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"information_schema_catalog_name","FullyQualifiedName":"localhost/postgres/information_schema_catalog_name","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"information_schema_catalog_name","FullyQualifiedName":"localhost/postgres/information_schema_catalog_name","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"key_column_usage","FullyQualifiedName":"localhost/postgres/key_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"key_column_usage","FullyQualifiedName":"localhost/postgres/key_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"key_column_usage","FullyQualifiedName":"localhost/postgres/key_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"key_column_usage","FullyQualifiedName":"localhost/postgres/key_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"key_column_usage","FullyQualifiedName":"localhost/postgres/key_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"key_column_usage","FullyQualifiedName":"localhost/postgres/key_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"key_column_usage","FullyQualifiedName":"localhost/postgres/key_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"parameters","FullyQualifiedName":"localhost/postgres/parameters","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"parameters","FullyQualifiedName":"localhost/postgres/parameters","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"parameters","FullyQualifiedName":"localhost/postgres/parameters","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"parameters","FullyQualifiedName":"localhost/postgres/parameters","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"parameters","FullyQualifiedName":"localhost/postgres/parameters","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"parameters","FullyQualifiedName":"localhost/postgres/parameters","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"parameters","FullyQualifiedName":"localhost/postgres/parameters","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_aggregate","FullyQualifiedName":"localhost/postgres/pg_aggregate","Type":"table","Metadata":{"rows":"157","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_aggregate","FullyQualifiedName":"localhost/postgres/pg_aggregate","Type":"table","Metadata":{"rows":"157","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_aggregate","FullyQualifiedName":"localhost/postgres/pg_aggregate","Type":"table","Metadata":{"rows":"157","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_aggregate","FullyQualifiedName":"localhost/postgres/pg_aggregate","Type":"table","Metadata":{"rows":"157","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_aggregate","FullyQualifiedName":"localhost/postgres/pg_aggregate","Type":"table","Metadata":{"rows":"157","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_aggregate","FullyQualifiedName":"localhost/postgres/pg_aggregate","Type":"table","Metadata":{"rows":"157","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_aggregate","FullyQualifiedName":"localhost/postgres/pg_aggregate","Type":"table","Metadata":{"rows":"157","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_am","FullyQualifiedName":"localhost/postgres/pg_am","Type":"table","Metadata":{"rows":"7","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_am","FullyQualifiedName":"localhost/postgres/pg_am","Type":"table","Metadata":{"rows":"7","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_am","FullyQualifiedName":"localhost/postgres/pg_am","Type":"table","Metadata":{"rows":"7","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_am","FullyQualifiedName":"localhost/postgres/pg_am","Type":"table","Metadata":{"rows":"7","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_am","FullyQualifiedName":"localhost/postgres/pg_am","Type":"table","Metadata":{"rows":"7","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_am","FullyQualifiedName":"localhost/postgres/pg_am","Type":"table","Metadata":{"rows":"7","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_am","FullyQualifiedName":"localhost/postgres/pg_am","Type":"table","Metadata":{"rows":"7","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_amop","FullyQualifiedName":"localhost/postgres/pg_amop","Type":"table","Metadata":{"rows":"945","size":"224 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_amop","FullyQualifiedName":"localhost/postgres/pg_amop","Type":"table","Metadata":{"rows":"945","size":"224 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_amop","FullyQualifiedName":"localhost/postgres/pg_amop","Type":"table","Metadata":{"rows":"945","size":"224 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_amop","FullyQualifiedName":"localhost/postgres/pg_amop","Type":"table","Metadata":{"rows":"945","size":"224 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_amop","FullyQualifiedName":"localhost/postgres/pg_amop","Type":"table","Metadata":{"rows":"945","size":"224 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_amop","FullyQualifiedName":"localhost/postgres/pg_amop","Type":"table","Metadata":{"rows":"945","size":"224 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_amop","FullyQualifiedName":"localhost/postgres/pg_amop","Type":"table","Metadata":{"rows":"945","size":"224 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_amproc","FullyQualifiedName":"localhost/postgres/pg_amproc","Type":"table","Metadata":{"rows":"696","size":"144 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_amproc","FullyQualifiedName":"localhost/postgres/pg_amproc","Type":"table","Metadata":{"rows":"696","size":"144 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_amproc","FullyQualifiedName":"localhost/postgres/pg_amproc","Type":"table","Metadata":{"rows":"696","size":"144 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_amproc","FullyQualifiedName":"localhost/postgres/pg_amproc","Type":"table","Metadata":{"rows":"696","size":"144 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_amproc","FullyQualifiedName":"localhost/postgres/pg_amproc","Type":"table","Metadata":{"rows":"696","size":"144 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_amproc","FullyQualifiedName":"localhost/postgres/pg_amproc","Type":"table","Metadata":{"rows":"696","size":"144 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_amproc","FullyQualifiedName":"localhost/postgres/pg_amproc","Type":"table","Metadata":{"rows":"696","size":"144 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_attrdef","FullyQualifiedName":"localhost/postgres/pg_attrdef","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_attrdef","FullyQualifiedName":"localhost/postgres/pg_attrdef","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_attrdef","FullyQualifiedName":"localhost/postgres/pg_attrdef","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_attrdef","FullyQualifiedName":"localhost/postgres/pg_attrdef","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_attrdef","FullyQualifiedName":"localhost/postgres/pg_attrdef","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_attrdef","FullyQualifiedName":"localhost/postgres/pg_attrdef","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_attrdef","FullyQualifiedName":"localhost/postgres/pg_attrdef","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_attribute","FullyQualifiedName":"localhost/postgres/pg_attribute","Type":"table","Metadata":{"rows":"3108","size":"704 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_attribute","FullyQualifiedName":"localhost/postgres/pg_attribute","Type":"table","Metadata":{"rows":"3108","size":"704 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_attribute","FullyQualifiedName":"localhost/postgres/pg_attribute","Type":"table","Metadata":{"rows":"3108","size":"704 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_attribute","FullyQualifiedName":"localhost/postgres/pg_attribute","Type":"table","Metadata":{"rows":"3108","size":"704 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_attribute","FullyQualifiedName":"localhost/postgres/pg_attribute","Type":"table","Metadata":{"rows":"3108","size":"704 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_attribute","FullyQualifiedName":"localhost/postgres/pg_attribute","Type":"table","Metadata":{"rows":"3108","size":"704 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_attribute","FullyQualifiedName":"localhost/postgres/pg_attribute","Type":"table","Metadata":{"rows":"3108","size":"704 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_auth_members","FullyQualifiedName":"localhost/postgres/pg_auth_members","Type":"table","Metadata":{"rows":"3","size":"104 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_auth_members","FullyQualifiedName":"localhost/postgres/pg_auth_members","Type":"table","Metadata":{"rows":"3","size":"104 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_auth_members","FullyQualifiedName":"localhost/postgres/pg_auth_members","Type":"table","Metadata":{"rows":"3","size":"104 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_auth_members","FullyQualifiedName":"localhost/postgres/pg_auth_members","Type":"table","Metadata":{"rows":"3","size":"104 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_auth_members","FullyQualifiedName":"localhost/postgres/pg_auth_members","Type":"table","Metadata":{"rows":"3","size":"104 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_auth_members","FullyQualifiedName":"localhost/postgres/pg_auth_members","Type":"table","Metadata":{"rows":"3","size":"104 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_auth_members","FullyQualifiedName":"localhost/postgres/pg_auth_members","Type":"table","Metadata":{"rows":"3","size":"104 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_authid","FullyQualifiedName":"localhost/postgres/pg_authid","Type":"table","Metadata":{"rows":"15","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_authid","FullyQualifiedName":"localhost/postgres/pg_authid","Type":"table","Metadata":{"rows":"15","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_authid","FullyQualifiedName":"localhost/postgres/pg_authid","Type":"table","Metadata":{"rows":"15","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_authid","FullyQualifiedName":"localhost/postgres/pg_authid","Type":"table","Metadata":{"rows":"15","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_authid","FullyQualifiedName":"localhost/postgres/pg_authid","Type":"table","Metadata":{"rows":"15","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_authid","FullyQualifiedName":"localhost/postgres/pg_authid","Type":"table","Metadata":{"rows":"15","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_authid","FullyQualifiedName":"localhost/postgres/pg_authid","Type":"table","Metadata":{"rows":"15","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_available_extension_versions","FullyQualifiedName":"localhost/postgres/pg_available_extension_versions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_available_extension_versions","FullyQualifiedName":"localhost/postgres/pg_available_extension_versions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_available_extension_versions","FullyQualifiedName":"localhost/postgres/pg_available_extension_versions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_available_extension_versions","FullyQualifiedName":"localhost/postgres/pg_available_extension_versions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_available_extension_versions","FullyQualifiedName":"localhost/postgres/pg_available_extension_versions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_available_extension_versions","FullyQualifiedName":"localhost/postgres/pg_available_extension_versions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_available_extension_versions","FullyQualifiedName":"localhost/postgres/pg_available_extension_versions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_available_extensions","FullyQualifiedName":"localhost/postgres/pg_available_extensions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_available_extensions","FullyQualifiedName":"localhost/postgres/pg_available_extensions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_available_extensions","FullyQualifiedName":"localhost/postgres/pg_available_extensions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_available_extensions","FullyQualifiedName":"localhost/postgres/pg_available_extensions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_available_extensions","FullyQualifiedName":"localhost/postgres/pg_available_extensions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_available_extensions","FullyQualifiedName":"localhost/postgres/pg_available_extensions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_available_extensions","FullyQualifiedName":"localhost/postgres/pg_available_extensions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_backend_memory_contexts","FullyQualifiedName":"localhost/postgres/pg_backend_memory_contexts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_backend_memory_contexts","FullyQualifiedName":"localhost/postgres/pg_backend_memory_contexts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_backend_memory_contexts","FullyQualifiedName":"localhost/postgres/pg_backend_memory_contexts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_backend_memory_contexts","FullyQualifiedName":"localhost/postgres/pg_backend_memory_contexts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_backend_memory_contexts","FullyQualifiedName":"localhost/postgres/pg_backend_memory_contexts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_backend_memory_contexts","FullyQualifiedName":"localhost/postgres/pg_backend_memory_contexts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_backend_memory_contexts","FullyQualifiedName":"localhost/postgres/pg_backend_memory_contexts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_cast","FullyQualifiedName":"localhost/postgres/pg_cast","Type":"table","Metadata":{"rows":"229","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_cast","FullyQualifiedName":"localhost/postgres/pg_cast","Type":"table","Metadata":{"rows":"229","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_cast","FullyQualifiedName":"localhost/postgres/pg_cast","Type":"table","Metadata":{"rows":"229","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_cast","FullyQualifiedName":"localhost/postgres/pg_cast","Type":"table","Metadata":{"rows":"229","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_cast","FullyQualifiedName":"localhost/postgres/pg_cast","Type":"table","Metadata":{"rows":"229","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_cast","FullyQualifiedName":"localhost/postgres/pg_cast","Type":"table","Metadata":{"rows":"229","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_cast","FullyQualifiedName":"localhost/postgres/pg_cast","Type":"table","Metadata":{"rows":"229","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_class","FullyQualifiedName":"localhost/postgres/pg_class","Type":"table","Metadata":{"rows":"413","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_class","FullyQualifiedName":"localhost/postgres/pg_class","Type":"table","Metadata":{"rows":"413","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_class","FullyQualifiedName":"localhost/postgres/pg_class","Type":"table","Metadata":{"rows":"413","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_class","FullyQualifiedName":"localhost/postgres/pg_class","Type":"table","Metadata":{"rows":"413","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_class","FullyQualifiedName":"localhost/postgres/pg_class","Type":"table","Metadata":{"rows":"413","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_class","FullyQualifiedName":"localhost/postgres/pg_class","Type":"table","Metadata":{"rows":"413","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_class","FullyQualifiedName":"localhost/postgres/pg_class","Type":"table","Metadata":{"rows":"413","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_collation","FullyQualifiedName":"localhost/postgres/pg_collation","Type":"table","Metadata":{"rows":"814","size":"240 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_collation","FullyQualifiedName":"localhost/postgres/pg_collation","Type":"table","Metadata":{"rows":"814","size":"240 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_collation","FullyQualifiedName":"localhost/postgres/pg_collation","Type":"table","Metadata":{"rows":"814","size":"240 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_collation","FullyQualifiedName":"localhost/postgres/pg_collation","Type":"table","Metadata":{"rows":"814","size":"240 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_collation","FullyQualifiedName":"localhost/postgres/pg_collation","Type":"table","Metadata":{"rows":"814","size":"240 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_collation","FullyQualifiedName":"localhost/postgres/pg_collation","Type":"table","Metadata":{"rows":"814","size":"240 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_collation","FullyQualifiedName":"localhost/postgres/pg_collation","Type":"table","Metadata":{"rows":"814","size":"240 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_config","FullyQualifiedName":"localhost/postgres/pg_config","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_config","FullyQualifiedName":"localhost/postgres/pg_config","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_config","FullyQualifiedName":"localhost/postgres/pg_config","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_config","FullyQualifiedName":"localhost/postgres/pg_config","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_config","FullyQualifiedName":"localhost/postgres/pg_config","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_config","FullyQualifiedName":"localhost/postgres/pg_config","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_config","FullyQualifiedName":"localhost/postgres/pg_config","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_constraint","FullyQualifiedName":"localhost/postgres/pg_constraint","Type":"table","Metadata":{"rows":"112","size":"144 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_constraint","FullyQualifiedName":"localhost/postgres/pg_constraint","Type":"table","Metadata":{"rows":"112","size":"144 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_constraint","FullyQualifiedName":"localhost/postgres/pg_constraint","Type":"table","Metadata":{"rows":"112","size":"144 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_constraint","FullyQualifiedName":"localhost/postgres/pg_constraint","Type":"table","Metadata":{"rows":"112","size":"144 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_constraint","FullyQualifiedName":"localhost/postgres/pg_constraint","Type":"table","Metadata":{"rows":"112","size":"144 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_constraint","FullyQualifiedName":"localhost/postgres/pg_constraint","Type":"table","Metadata":{"rows":"112","size":"144 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_constraint","FullyQualifiedName":"localhost/postgres/pg_constraint","Type":"table","Metadata":{"rows":"112","size":"144 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_conversion","FullyQualifiedName":"localhost/postgres/pg_conversion","Type":"table","Metadata":{"rows":"128","size":"96 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_conversion","FullyQualifiedName":"localhost/postgres/pg_conversion","Type":"table","Metadata":{"rows":"128","size":"96 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_conversion","FullyQualifiedName":"localhost/postgres/pg_conversion","Type":"table","Metadata":{"rows":"128","size":"96 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_conversion","FullyQualifiedName":"localhost/postgres/pg_conversion","Type":"table","Metadata":{"rows":"128","size":"96 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_conversion","FullyQualifiedName":"localhost/postgres/pg_conversion","Type":"table","Metadata":{"rows":"128","size":"96 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_conversion","FullyQualifiedName":"localhost/postgres/pg_conversion","Type":"table","Metadata":{"rows":"128","size":"96 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_conversion","FullyQualifiedName":"localhost/postgres/pg_conversion","Type":"table","Metadata":{"rows":"128","size":"96 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_cursors","FullyQualifiedName":"localhost/postgres/pg_cursors","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_cursors","FullyQualifiedName":"localhost/postgres/pg_cursors","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_cursors","FullyQualifiedName":"localhost/postgres/pg_cursors","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_cursors","FullyQualifiedName":"localhost/postgres/pg_cursors","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_cursors","FullyQualifiedName":"localhost/postgres/pg_cursors","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_cursors","FullyQualifiedName":"localhost/postgres/pg_cursors","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_cursors","FullyQualifiedName":"localhost/postgres/pg_cursors","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_database","FullyQualifiedName":"localhost/postgres/pg_database","Type":"table","Metadata":{"rows":"2","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_database","FullyQualifiedName":"localhost/postgres/pg_database","Type":"table","Metadata":{"rows":"2","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_database","FullyQualifiedName":"localhost/postgres/pg_database","Type":"table","Metadata":{"rows":"2","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_database","FullyQualifiedName":"localhost/postgres/pg_database","Type":"table","Metadata":{"rows":"2","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_database","FullyQualifiedName":"localhost/postgres/pg_database","Type":"table","Metadata":{"rows":"2","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_database","FullyQualifiedName":"localhost/postgres/pg_database","Type":"table","Metadata":{"rows":"2","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_database","FullyQualifiedName":"localhost/postgres/pg_database","Type":"table","Metadata":{"rows":"2","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_db_role_setting","FullyQualifiedName":"localhost/postgres/pg_db_role_setting","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_db_role_setting","FullyQualifiedName":"localhost/postgres/pg_db_role_setting","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_db_role_setting","FullyQualifiedName":"localhost/postgres/pg_db_role_setting","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_db_role_setting","FullyQualifiedName":"localhost/postgres/pg_db_role_setting","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_db_role_setting","FullyQualifiedName":"localhost/postgres/pg_db_role_setting","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_db_role_setting","FullyQualifiedName":"localhost/postgres/pg_db_role_setting","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_db_role_setting","FullyQualifiedName":"localhost/postgres/pg_db_role_setting","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_default_acl","FullyQualifiedName":"localhost/postgres/pg_default_acl","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_default_acl","FullyQualifiedName":"localhost/postgres/pg_default_acl","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_default_acl","FullyQualifiedName":"localhost/postgres/pg_default_acl","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_default_acl","FullyQualifiedName":"localhost/postgres/pg_default_acl","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_default_acl","FullyQualifiedName":"localhost/postgres/pg_default_acl","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_default_acl","FullyQualifiedName":"localhost/postgres/pg_default_acl","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_default_acl","FullyQualifiedName":"localhost/postgres/pg_default_acl","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_depend","FullyQualifiedName":"localhost/postgres/pg_depend","Type":"table","Metadata":{"rows":"1704","size":"272 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_depend","FullyQualifiedName":"localhost/postgres/pg_depend","Type":"table","Metadata":{"rows":"1704","size":"272 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_depend","FullyQualifiedName":"localhost/postgres/pg_depend","Type":"table","Metadata":{"rows":"1704","size":"272 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_depend","FullyQualifiedName":"localhost/postgres/pg_depend","Type":"table","Metadata":{"rows":"1704","size":"272 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_depend","FullyQualifiedName":"localhost/postgres/pg_depend","Type":"table","Metadata":{"rows":"1704","size":"272 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_depend","FullyQualifiedName":"localhost/postgres/pg_depend","Type":"table","Metadata":{"rows":"1704","size":"272 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_depend","FullyQualifiedName":"localhost/postgres/pg_depend","Type":"table","Metadata":{"rows":"1704","size":"272 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_description","FullyQualifiedName":"localhost/postgres/pg_description","Type":"table","Metadata":{"rows":"5191","size":"616 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_description","FullyQualifiedName":"localhost/postgres/pg_description","Type":"table","Metadata":{"rows":"5191","size":"616 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_description","FullyQualifiedName":"localhost/postgres/pg_description","Type":"table","Metadata":{"rows":"5191","size":"616 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_description","FullyQualifiedName":"localhost/postgres/pg_description","Type":"table","Metadata":{"rows":"5191","size":"616 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_description","FullyQualifiedName":"localhost/postgres/pg_description","Type":"table","Metadata":{"rows":"5191","size":"616 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_description","FullyQualifiedName":"localhost/postgres/pg_description","Type":"table","Metadata":{"rows":"5191","size":"616 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_description","FullyQualifiedName":"localhost/postgres/pg_description","Type":"table","Metadata":{"rows":"5191","size":"616 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_enum","FullyQualifiedName":"localhost/postgres/pg_enum","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_enum","FullyQualifiedName":"localhost/postgres/pg_enum","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_enum","FullyQualifiedName":"localhost/postgres/pg_enum","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_enum","FullyQualifiedName":"localhost/postgres/pg_enum","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_enum","FullyQualifiedName":"localhost/postgres/pg_enum","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_enum","FullyQualifiedName":"localhost/postgres/pg_enum","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_enum","FullyQualifiedName":"localhost/postgres/pg_enum","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_event_trigger","FullyQualifiedName":"localhost/postgres/pg_event_trigger","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_event_trigger","FullyQualifiedName":"localhost/postgres/pg_event_trigger","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_event_trigger","FullyQualifiedName":"localhost/postgres/pg_event_trigger","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_event_trigger","FullyQualifiedName":"localhost/postgres/pg_event_trigger","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_event_trigger","FullyQualifiedName":"localhost/postgres/pg_event_trigger","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_event_trigger","FullyQualifiedName":"localhost/postgres/pg_event_trigger","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_event_trigger","FullyQualifiedName":"localhost/postgres/pg_event_trigger","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_extension","FullyQualifiedName":"localhost/postgres/pg_extension","Type":"table","Metadata":{"rows":"1","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_extension","FullyQualifiedName":"localhost/postgres/pg_extension","Type":"table","Metadata":{"rows":"1","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_extension","FullyQualifiedName":"localhost/postgres/pg_extension","Type":"table","Metadata":{"rows":"1","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_extension","FullyQualifiedName":"localhost/postgres/pg_extension","Type":"table","Metadata":{"rows":"1","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_extension","FullyQualifiedName":"localhost/postgres/pg_extension","Type":"table","Metadata":{"rows":"1","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_extension","FullyQualifiedName":"localhost/postgres/pg_extension","Type":"table","Metadata":{"rows":"1","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_extension","FullyQualifiedName":"localhost/postgres/pg_extension","Type":"table","Metadata":{"rows":"1","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_file_settings","FullyQualifiedName":"localhost/postgres/pg_file_settings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_file_settings","FullyQualifiedName":"localhost/postgres/pg_file_settings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_file_settings","FullyQualifiedName":"localhost/postgres/pg_file_settings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_file_settings","FullyQualifiedName":"localhost/postgres/pg_file_settings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_file_settings","FullyQualifiedName":"localhost/postgres/pg_file_settings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_file_settings","FullyQualifiedName":"localhost/postgres/pg_file_settings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_file_settings","FullyQualifiedName":"localhost/postgres/pg_file_settings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_foreign_data_wrapper","FullyQualifiedName":"localhost/postgres/pg_foreign_data_wrapper","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_foreign_data_wrapper","FullyQualifiedName":"localhost/postgres/pg_foreign_data_wrapper","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_foreign_data_wrapper","FullyQualifiedName":"localhost/postgres/pg_foreign_data_wrapper","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_foreign_data_wrapper","FullyQualifiedName":"localhost/postgres/pg_foreign_data_wrapper","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_foreign_data_wrapper","FullyQualifiedName":"localhost/postgres/pg_foreign_data_wrapper","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_foreign_data_wrapper","FullyQualifiedName":"localhost/postgres/pg_foreign_data_wrapper","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_foreign_data_wrapper","FullyQualifiedName":"localhost/postgres/pg_foreign_data_wrapper","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_foreign_server","FullyQualifiedName":"localhost/postgres/pg_foreign_server","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_foreign_server","FullyQualifiedName":"localhost/postgres/pg_foreign_server","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_foreign_server","FullyQualifiedName":"localhost/postgres/pg_foreign_server","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_foreign_server","FullyQualifiedName":"localhost/postgres/pg_foreign_server","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_foreign_server","FullyQualifiedName":"localhost/postgres/pg_foreign_server","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_foreign_server","FullyQualifiedName":"localhost/postgres/pg_foreign_server","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_foreign_server","FullyQualifiedName":"localhost/postgres/pg_foreign_server","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_foreign_table","FullyQualifiedName":"localhost/postgres/pg_foreign_table","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_foreign_table","FullyQualifiedName":"localhost/postgres/pg_foreign_table","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_foreign_table","FullyQualifiedName":"localhost/postgres/pg_foreign_table","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_foreign_table","FullyQualifiedName":"localhost/postgres/pg_foreign_table","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_foreign_table","FullyQualifiedName":"localhost/postgres/pg_foreign_table","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_foreign_table","FullyQualifiedName":"localhost/postgres/pg_foreign_table","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_foreign_table","FullyQualifiedName":"localhost/postgres/pg_foreign_table","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_group","FullyQualifiedName":"localhost/postgres/pg_group","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_group","FullyQualifiedName":"localhost/postgres/pg_group","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_group","FullyQualifiedName":"localhost/postgres/pg_group","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_group","FullyQualifiedName":"localhost/postgres/pg_group","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_group","FullyQualifiedName":"localhost/postgres/pg_group","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_group","FullyQualifiedName":"localhost/postgres/pg_group","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_group","FullyQualifiedName":"localhost/postgres/pg_group","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_hba_file_rules","FullyQualifiedName":"localhost/postgres/pg_hba_file_rules","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_hba_file_rules","FullyQualifiedName":"localhost/postgres/pg_hba_file_rules","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_hba_file_rules","FullyQualifiedName":"localhost/postgres/pg_hba_file_rules","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_hba_file_rules","FullyQualifiedName":"localhost/postgres/pg_hba_file_rules","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_hba_file_rules","FullyQualifiedName":"localhost/postgres/pg_hba_file_rules","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_hba_file_rules","FullyQualifiedName":"localhost/postgres/pg_hba_file_rules","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_hba_file_rules","FullyQualifiedName":"localhost/postgres/pg_hba_file_rules","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_ident_file_mappings","FullyQualifiedName":"localhost/postgres/pg_ident_file_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_ident_file_mappings","FullyQualifiedName":"localhost/postgres/pg_ident_file_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_ident_file_mappings","FullyQualifiedName":"localhost/postgres/pg_ident_file_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_ident_file_mappings","FullyQualifiedName":"localhost/postgres/pg_ident_file_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_ident_file_mappings","FullyQualifiedName":"localhost/postgres/pg_ident_file_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_ident_file_mappings","FullyQualifiedName":"localhost/postgres/pg_ident_file_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_ident_file_mappings","FullyQualifiedName":"localhost/postgres/pg_ident_file_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_index","FullyQualifiedName":"localhost/postgres/pg_index","Type":"table","Metadata":{"rows":"164","size":"96 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_index","FullyQualifiedName":"localhost/postgres/pg_index","Type":"table","Metadata":{"rows":"164","size":"96 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_index","FullyQualifiedName":"localhost/postgres/pg_index","Type":"table","Metadata":{"rows":"164","size":"96 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_index","FullyQualifiedName":"localhost/postgres/pg_index","Type":"table","Metadata":{"rows":"164","size":"96 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_index","FullyQualifiedName":"localhost/postgres/pg_index","Type":"table","Metadata":{"rows":"164","size":"96 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_index","FullyQualifiedName":"localhost/postgres/pg_index","Type":"table","Metadata":{"rows":"164","size":"96 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_index","FullyQualifiedName":"localhost/postgres/pg_index","Type":"table","Metadata":{"rows":"164","size":"96 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_indexes","FullyQualifiedName":"localhost/postgres/pg_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_indexes","FullyQualifiedName":"localhost/postgres/pg_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_indexes","FullyQualifiedName":"localhost/postgres/pg_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_indexes","FullyQualifiedName":"localhost/postgres/pg_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_indexes","FullyQualifiedName":"localhost/postgres/pg_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_indexes","FullyQualifiedName":"localhost/postgres/pg_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_indexes","FullyQualifiedName":"localhost/postgres/pg_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_inherits","FullyQualifiedName":"localhost/postgres/pg_inherits","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_inherits","FullyQualifiedName":"localhost/postgres/pg_inherits","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_inherits","FullyQualifiedName":"localhost/postgres/pg_inherits","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_inherits","FullyQualifiedName":"localhost/postgres/pg_inherits","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_inherits","FullyQualifiedName":"localhost/postgres/pg_inherits","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_inherits","FullyQualifiedName":"localhost/postgres/pg_inherits","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_inherits","FullyQualifiedName":"localhost/postgres/pg_inherits","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_init_privs","FullyQualifiedName":"localhost/postgres/pg_init_privs","Type":"table","Metadata":{"rows":"220","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_init_privs","FullyQualifiedName":"localhost/postgres/pg_init_privs","Type":"table","Metadata":{"rows":"220","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_init_privs","FullyQualifiedName":"localhost/postgres/pg_init_privs","Type":"table","Metadata":{"rows":"220","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_init_privs","FullyQualifiedName":"localhost/postgres/pg_init_privs","Type":"table","Metadata":{"rows":"220","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_init_privs","FullyQualifiedName":"localhost/postgres/pg_init_privs","Type":"table","Metadata":{"rows":"220","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_init_privs","FullyQualifiedName":"localhost/postgres/pg_init_privs","Type":"table","Metadata":{"rows":"220","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_init_privs","FullyQualifiedName":"localhost/postgres/pg_init_privs","Type":"table","Metadata":{"rows":"220","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_language","FullyQualifiedName":"localhost/postgres/pg_language","Type":"table","Metadata":{"rows":"4","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_language","FullyQualifiedName":"localhost/postgres/pg_language","Type":"table","Metadata":{"rows":"4","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_language","FullyQualifiedName":"localhost/postgres/pg_language","Type":"table","Metadata":{"rows":"4","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_language","FullyQualifiedName":"localhost/postgres/pg_language","Type":"table","Metadata":{"rows":"4","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_language","FullyQualifiedName":"localhost/postgres/pg_language","Type":"table","Metadata":{"rows":"4","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_language","FullyQualifiedName":"localhost/postgres/pg_language","Type":"table","Metadata":{"rows":"4","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_language","FullyQualifiedName":"localhost/postgres/pg_language","Type":"table","Metadata":{"rows":"4","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_largeobject","FullyQualifiedName":"localhost/postgres/pg_largeobject","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_largeobject","FullyQualifiedName":"localhost/postgres/pg_largeobject","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_largeobject","FullyQualifiedName":"localhost/postgres/pg_largeobject","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_largeobject","FullyQualifiedName":"localhost/postgres/pg_largeobject","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_largeobject","FullyQualifiedName":"localhost/postgres/pg_largeobject","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_largeobject","FullyQualifiedName":"localhost/postgres/pg_largeobject","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_largeobject","FullyQualifiedName":"localhost/postgres/pg_largeobject","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_largeobject_metadata","FullyQualifiedName":"localhost/postgres/pg_largeobject_metadata","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_largeobject_metadata","FullyQualifiedName":"localhost/postgres/pg_largeobject_metadata","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_largeobject_metadata","FullyQualifiedName":"localhost/postgres/pg_largeobject_metadata","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_largeobject_metadata","FullyQualifiedName":"localhost/postgres/pg_largeobject_metadata","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_largeobject_metadata","FullyQualifiedName":"localhost/postgres/pg_largeobject_metadata","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_largeobject_metadata","FullyQualifiedName":"localhost/postgres/pg_largeobject_metadata","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_largeobject_metadata","FullyQualifiedName":"localhost/postgres/pg_largeobject_metadata","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_locks","FullyQualifiedName":"localhost/postgres/pg_locks","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_locks","FullyQualifiedName":"localhost/postgres/pg_locks","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_locks","FullyQualifiedName":"localhost/postgres/pg_locks","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_locks","FullyQualifiedName":"localhost/postgres/pg_locks","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_locks","FullyQualifiedName":"localhost/postgres/pg_locks","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_locks","FullyQualifiedName":"localhost/postgres/pg_locks","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_locks","FullyQualifiedName":"localhost/postgres/pg_locks","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_matviews","FullyQualifiedName":"localhost/postgres/pg_matviews","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_matviews","FullyQualifiedName":"localhost/postgres/pg_matviews","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_matviews","FullyQualifiedName":"localhost/postgres/pg_matviews","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_matviews","FullyQualifiedName":"localhost/postgres/pg_matviews","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_matviews","FullyQualifiedName":"localhost/postgres/pg_matviews","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_matviews","FullyQualifiedName":"localhost/postgres/pg_matviews","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_matviews","FullyQualifiedName":"localhost/postgres/pg_matviews","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_namespace","FullyQualifiedName":"localhost/postgres/pg_namespace","Type":"table","Metadata":{"rows":"4","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_namespace","FullyQualifiedName":"localhost/postgres/pg_namespace","Type":"table","Metadata":{"rows":"4","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_namespace","FullyQualifiedName":"localhost/postgres/pg_namespace","Type":"table","Metadata":{"rows":"4","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_namespace","FullyQualifiedName":"localhost/postgres/pg_namespace","Type":"table","Metadata":{"rows":"4","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_namespace","FullyQualifiedName":"localhost/postgres/pg_namespace","Type":"table","Metadata":{"rows":"4","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_namespace","FullyQualifiedName":"localhost/postgres/pg_namespace","Type":"table","Metadata":{"rows":"4","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_namespace","FullyQualifiedName":"localhost/postgres/pg_namespace","Type":"table","Metadata":{"rows":"4","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_opclass","FullyQualifiedName":"localhost/postgres/pg_opclass","Type":"table","Metadata":{"rows":"177","size":"88 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_opclass","FullyQualifiedName":"localhost/postgres/pg_opclass","Type":"table","Metadata":{"rows":"177","size":"88 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_opclass","FullyQualifiedName":"localhost/postgres/pg_opclass","Type":"table","Metadata":{"rows":"177","size":"88 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_opclass","FullyQualifiedName":"localhost/postgres/pg_opclass","Type":"table","Metadata":{"rows":"177","size":"88 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_opclass","FullyQualifiedName":"localhost/postgres/pg_opclass","Type":"table","Metadata":{"rows":"177","size":"88 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_opclass","FullyQualifiedName":"localhost/postgres/pg_opclass","Type":"table","Metadata":{"rows":"177","size":"88 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_opclass","FullyQualifiedName":"localhost/postgres/pg_opclass","Type":"table","Metadata":{"rows":"177","size":"88 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_operator","FullyQualifiedName":"localhost/postgres/pg_operator","Type":"table","Metadata":{"rows":"799","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_operator","FullyQualifiedName":"localhost/postgres/pg_operator","Type":"table","Metadata":{"rows":"799","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_operator","FullyQualifiedName":"localhost/postgres/pg_operator","Type":"table","Metadata":{"rows":"799","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_operator","FullyQualifiedName":"localhost/postgres/pg_operator","Type":"table","Metadata":{"rows":"799","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_operator","FullyQualifiedName":"localhost/postgres/pg_operator","Type":"table","Metadata":{"rows":"799","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_operator","FullyQualifiedName":"localhost/postgres/pg_operator","Type":"table","Metadata":{"rows":"799","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_operator","FullyQualifiedName":"localhost/postgres/pg_operator","Type":"table","Metadata":{"rows":"799","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_opfamily","FullyQualifiedName":"localhost/postgres/pg_opfamily","Type":"table","Metadata":{"rows":"146","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_opfamily","FullyQualifiedName":"localhost/postgres/pg_opfamily","Type":"table","Metadata":{"rows":"146","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_opfamily","FullyQualifiedName":"localhost/postgres/pg_opfamily","Type":"table","Metadata":{"rows":"146","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_opfamily","FullyQualifiedName":"localhost/postgres/pg_opfamily","Type":"table","Metadata":{"rows":"146","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_opfamily","FullyQualifiedName":"localhost/postgres/pg_opfamily","Type":"table","Metadata":{"rows":"146","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_opfamily","FullyQualifiedName":"localhost/postgres/pg_opfamily","Type":"table","Metadata":{"rows":"146","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_opfamily","FullyQualifiedName":"localhost/postgres/pg_opfamily","Type":"table","Metadata":{"rows":"146","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_parameter_acl","FullyQualifiedName":"localhost/postgres/pg_parameter_acl","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_parameter_acl","FullyQualifiedName":"localhost/postgres/pg_parameter_acl","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_parameter_acl","FullyQualifiedName":"localhost/postgres/pg_parameter_acl","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_parameter_acl","FullyQualifiedName":"localhost/postgres/pg_parameter_acl","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_parameter_acl","FullyQualifiedName":"localhost/postgres/pg_parameter_acl","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_parameter_acl","FullyQualifiedName":"localhost/postgres/pg_parameter_acl","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_parameter_acl","FullyQualifiedName":"localhost/postgres/pg_parameter_acl","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_partitioned_table","FullyQualifiedName":"localhost/postgres/pg_partitioned_table","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_partitioned_table","FullyQualifiedName":"localhost/postgres/pg_partitioned_table","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_partitioned_table","FullyQualifiedName":"localhost/postgres/pg_partitioned_table","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_partitioned_table","FullyQualifiedName":"localhost/postgres/pg_partitioned_table","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_partitioned_table","FullyQualifiedName":"localhost/postgres/pg_partitioned_table","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_partitioned_table","FullyQualifiedName":"localhost/postgres/pg_partitioned_table","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_partitioned_table","FullyQualifiedName":"localhost/postgres/pg_partitioned_table","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_policies","FullyQualifiedName":"localhost/postgres/pg_policies","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_policies","FullyQualifiedName":"localhost/postgres/pg_policies","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_policies","FullyQualifiedName":"localhost/postgres/pg_policies","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_policies","FullyQualifiedName":"localhost/postgres/pg_policies","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_policies","FullyQualifiedName":"localhost/postgres/pg_policies","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_policies","FullyQualifiedName":"localhost/postgres/pg_policies","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_policies","FullyQualifiedName":"localhost/postgres/pg_policies","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_policy","FullyQualifiedName":"localhost/postgres/pg_policy","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_policy","FullyQualifiedName":"localhost/postgres/pg_policy","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_policy","FullyQualifiedName":"localhost/postgres/pg_policy","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_policy","FullyQualifiedName":"localhost/postgres/pg_policy","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_policy","FullyQualifiedName":"localhost/postgres/pg_policy","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_policy","FullyQualifiedName":"localhost/postgres/pg_policy","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_policy","FullyQualifiedName":"localhost/postgres/pg_policy","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_prepared_statements","FullyQualifiedName":"localhost/postgres/pg_prepared_statements","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_prepared_statements","FullyQualifiedName":"localhost/postgres/pg_prepared_statements","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_prepared_statements","FullyQualifiedName":"localhost/postgres/pg_prepared_statements","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_prepared_statements","FullyQualifiedName":"localhost/postgres/pg_prepared_statements","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_prepared_statements","FullyQualifiedName":"localhost/postgres/pg_prepared_statements","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_prepared_statements","FullyQualifiedName":"localhost/postgres/pg_prepared_statements","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_prepared_statements","FullyQualifiedName":"localhost/postgres/pg_prepared_statements","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_prepared_xacts","FullyQualifiedName":"localhost/postgres/pg_prepared_xacts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_prepared_xacts","FullyQualifiedName":"localhost/postgres/pg_prepared_xacts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_prepared_xacts","FullyQualifiedName":"localhost/postgres/pg_prepared_xacts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_prepared_xacts","FullyQualifiedName":"localhost/postgres/pg_prepared_xacts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_prepared_xacts","FullyQualifiedName":"localhost/postgres/pg_prepared_xacts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_prepared_xacts","FullyQualifiedName":"localhost/postgres/pg_prepared_xacts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_prepared_xacts","FullyQualifiedName":"localhost/postgres/pg_prepared_xacts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_proc","FullyQualifiedName":"localhost/postgres/pg_proc","Type":"table","Metadata":{"rows":"3297","size":"1216 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_proc","FullyQualifiedName":"localhost/postgres/pg_proc","Type":"table","Metadata":{"rows":"3297","size":"1216 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_proc","FullyQualifiedName":"localhost/postgres/pg_proc","Type":"table","Metadata":{"rows":"3297","size":"1216 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_proc","FullyQualifiedName":"localhost/postgres/pg_proc","Type":"table","Metadata":{"rows":"3297","size":"1216 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_proc","FullyQualifiedName":"localhost/postgres/pg_proc","Type":"table","Metadata":{"rows":"3297","size":"1216 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_proc","FullyQualifiedName":"localhost/postgres/pg_proc","Type":"table","Metadata":{"rows":"3297","size":"1216 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_proc","FullyQualifiedName":"localhost/postgres/pg_proc","Type":"table","Metadata":{"rows":"3297","size":"1216 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_publication","FullyQualifiedName":"localhost/postgres/pg_publication","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_publication","FullyQualifiedName":"localhost/postgres/pg_publication","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_publication","FullyQualifiedName":"localhost/postgres/pg_publication","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_publication","FullyQualifiedName":"localhost/postgres/pg_publication","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_publication","FullyQualifiedName":"localhost/postgres/pg_publication","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_publication","FullyQualifiedName":"localhost/postgres/pg_publication","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_publication","FullyQualifiedName":"localhost/postgres/pg_publication","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_publication_namespace","FullyQualifiedName":"localhost/postgres/pg_publication_namespace","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_publication_namespace","FullyQualifiedName":"localhost/postgres/pg_publication_namespace","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_publication_namespace","FullyQualifiedName":"localhost/postgres/pg_publication_namespace","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_publication_namespace","FullyQualifiedName":"localhost/postgres/pg_publication_namespace","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_publication_namespace","FullyQualifiedName":"localhost/postgres/pg_publication_namespace","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_publication_namespace","FullyQualifiedName":"localhost/postgres/pg_publication_namespace","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_publication_namespace","FullyQualifiedName":"localhost/postgres/pg_publication_namespace","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_publication_rel","FullyQualifiedName":"localhost/postgres/pg_publication_rel","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_publication_rel","FullyQualifiedName":"localhost/postgres/pg_publication_rel","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_publication_rel","FullyQualifiedName":"localhost/postgres/pg_publication_rel","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_publication_rel","FullyQualifiedName":"localhost/postgres/pg_publication_rel","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_publication_rel","FullyQualifiedName":"localhost/postgres/pg_publication_rel","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_publication_rel","FullyQualifiedName":"localhost/postgres/pg_publication_rel","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_publication_rel","FullyQualifiedName":"localhost/postgres/pg_publication_rel","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_publication_tables","FullyQualifiedName":"localhost/postgres/pg_publication_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_publication_tables","FullyQualifiedName":"localhost/postgres/pg_publication_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_publication_tables","FullyQualifiedName":"localhost/postgres/pg_publication_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_publication_tables","FullyQualifiedName":"localhost/postgres/pg_publication_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_publication_tables","FullyQualifiedName":"localhost/postgres/pg_publication_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_publication_tables","FullyQualifiedName":"localhost/postgres/pg_publication_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_publication_tables","FullyQualifiedName":"localhost/postgres/pg_publication_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_range","FullyQualifiedName":"localhost/postgres/pg_range","Type":"table","Metadata":{"rows":"6","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_range","FullyQualifiedName":"localhost/postgres/pg_range","Type":"table","Metadata":{"rows":"6","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_range","FullyQualifiedName":"localhost/postgres/pg_range","Type":"table","Metadata":{"rows":"6","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_range","FullyQualifiedName":"localhost/postgres/pg_range","Type":"table","Metadata":{"rows":"6","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_range","FullyQualifiedName":"localhost/postgres/pg_range","Type":"table","Metadata":{"rows":"6","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_range","FullyQualifiedName":"localhost/postgres/pg_range","Type":"table","Metadata":{"rows":"6","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_range","FullyQualifiedName":"localhost/postgres/pg_range","Type":"table","Metadata":{"rows":"6","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_replication_origin","FullyQualifiedName":"localhost/postgres/pg_replication_origin","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_replication_origin","FullyQualifiedName":"localhost/postgres/pg_replication_origin","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_replication_origin","FullyQualifiedName":"localhost/postgres/pg_replication_origin","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_replication_origin","FullyQualifiedName":"localhost/postgres/pg_replication_origin","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_replication_origin","FullyQualifiedName":"localhost/postgres/pg_replication_origin","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_replication_origin","FullyQualifiedName":"localhost/postgres/pg_replication_origin","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_replication_origin","FullyQualifiedName":"localhost/postgres/pg_replication_origin","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_replication_origin_status","FullyQualifiedName":"localhost/postgres/pg_replication_origin_status","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_replication_origin_status","FullyQualifiedName":"localhost/postgres/pg_replication_origin_status","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_replication_origin_status","FullyQualifiedName":"localhost/postgres/pg_replication_origin_status","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_replication_origin_status","FullyQualifiedName":"localhost/postgres/pg_replication_origin_status","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_replication_origin_status","FullyQualifiedName":"localhost/postgres/pg_replication_origin_status","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_replication_origin_status","FullyQualifiedName":"localhost/postgres/pg_replication_origin_status","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_replication_origin_status","FullyQualifiedName":"localhost/postgres/pg_replication_origin_status","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_replication_slots","FullyQualifiedName":"localhost/postgres/pg_replication_slots","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_replication_slots","FullyQualifiedName":"localhost/postgres/pg_replication_slots","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_replication_slots","FullyQualifiedName":"localhost/postgres/pg_replication_slots","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_replication_slots","FullyQualifiedName":"localhost/postgres/pg_replication_slots","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_replication_slots","FullyQualifiedName":"localhost/postgres/pg_replication_slots","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_replication_slots","FullyQualifiedName":"localhost/postgres/pg_replication_slots","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_replication_slots","FullyQualifiedName":"localhost/postgres/pg_replication_slots","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_rewrite","FullyQualifiedName":"localhost/postgres/pg_rewrite","Type":"table","Metadata":{"rows":"143","size":"728 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_rewrite","FullyQualifiedName":"localhost/postgres/pg_rewrite","Type":"table","Metadata":{"rows":"143","size":"728 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_rewrite","FullyQualifiedName":"localhost/postgres/pg_rewrite","Type":"table","Metadata":{"rows":"143","size":"728 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_rewrite","FullyQualifiedName":"localhost/postgres/pg_rewrite","Type":"table","Metadata":{"rows":"143","size":"728 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_rewrite","FullyQualifiedName":"localhost/postgres/pg_rewrite","Type":"table","Metadata":{"rows":"143","size":"728 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_rewrite","FullyQualifiedName":"localhost/postgres/pg_rewrite","Type":"table","Metadata":{"rows":"143","size":"728 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_rewrite","FullyQualifiedName":"localhost/postgres/pg_rewrite","Type":"table","Metadata":{"rows":"143","size":"728 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_roles","FullyQualifiedName":"localhost/postgres/pg_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_roles","FullyQualifiedName":"localhost/postgres/pg_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_roles","FullyQualifiedName":"localhost/postgres/pg_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_roles","FullyQualifiedName":"localhost/postgres/pg_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_roles","FullyQualifiedName":"localhost/postgres/pg_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_roles","FullyQualifiedName":"localhost/postgres/pg_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_roles","FullyQualifiedName":"localhost/postgres/pg_roles","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_rules","FullyQualifiedName":"localhost/postgres/pg_rules","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_rules","FullyQualifiedName":"localhost/postgres/pg_rules","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_rules","FullyQualifiedName":"localhost/postgres/pg_rules","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_rules","FullyQualifiedName":"localhost/postgres/pg_rules","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_rules","FullyQualifiedName":"localhost/postgres/pg_rules","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_rules","FullyQualifiedName":"localhost/postgres/pg_rules","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_rules","FullyQualifiedName":"localhost/postgres/pg_rules","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_seclabel","FullyQualifiedName":"localhost/postgres/pg_seclabel","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_seclabel","FullyQualifiedName":"localhost/postgres/pg_seclabel","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_seclabel","FullyQualifiedName":"localhost/postgres/pg_seclabel","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_seclabel","FullyQualifiedName":"localhost/postgres/pg_seclabel","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_seclabel","FullyQualifiedName":"localhost/postgres/pg_seclabel","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_seclabel","FullyQualifiedName":"localhost/postgres/pg_seclabel","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_seclabel","FullyQualifiedName":"localhost/postgres/pg_seclabel","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_seclabels","FullyQualifiedName":"localhost/postgres/pg_seclabels","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_seclabels","FullyQualifiedName":"localhost/postgres/pg_seclabels","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_seclabels","FullyQualifiedName":"localhost/postgres/pg_seclabels","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_seclabels","FullyQualifiedName":"localhost/postgres/pg_seclabels","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_seclabels","FullyQualifiedName":"localhost/postgres/pg_seclabels","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_seclabels","FullyQualifiedName":"localhost/postgres/pg_seclabels","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_seclabels","FullyQualifiedName":"localhost/postgres/pg_seclabels","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_sequence","FullyQualifiedName":"localhost/postgres/pg_sequence","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_sequence","FullyQualifiedName":"localhost/postgres/pg_sequence","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_sequence","FullyQualifiedName":"localhost/postgres/pg_sequence","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_sequence","FullyQualifiedName":"localhost/postgres/pg_sequence","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_sequence","FullyQualifiedName":"localhost/postgres/pg_sequence","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_sequence","FullyQualifiedName":"localhost/postgres/pg_sequence","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_sequence","FullyQualifiedName":"localhost/postgres/pg_sequence","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_sequences","FullyQualifiedName":"localhost/postgres/pg_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_sequences","FullyQualifiedName":"localhost/postgres/pg_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_sequences","FullyQualifiedName":"localhost/postgres/pg_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_sequences","FullyQualifiedName":"localhost/postgres/pg_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_sequences","FullyQualifiedName":"localhost/postgres/pg_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_sequences","FullyQualifiedName":"localhost/postgres/pg_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_sequences","FullyQualifiedName":"localhost/postgres/pg_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_settings","FullyQualifiedName":"localhost/postgres/pg_settings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_settings","FullyQualifiedName":"localhost/postgres/pg_settings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_settings","FullyQualifiedName":"localhost/postgres/pg_settings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_settings","FullyQualifiedName":"localhost/postgres/pg_settings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_settings","FullyQualifiedName":"localhost/postgres/pg_settings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_settings","FullyQualifiedName":"localhost/postgres/pg_settings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_settings","FullyQualifiedName":"localhost/postgres/pg_settings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_shadow","FullyQualifiedName":"localhost/postgres/pg_shadow","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_shadow","FullyQualifiedName":"localhost/postgres/pg_shadow","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_shadow","FullyQualifiedName":"localhost/postgres/pg_shadow","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_shadow","FullyQualifiedName":"localhost/postgres/pg_shadow","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_shadow","FullyQualifiedName":"localhost/postgres/pg_shadow","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_shadow","FullyQualifiedName":"localhost/postgres/pg_shadow","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_shadow","FullyQualifiedName":"localhost/postgres/pg_shadow","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_shdepend","FullyQualifiedName":"localhost/postgres/pg_shdepend","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_shdepend","FullyQualifiedName":"localhost/postgres/pg_shdepend","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_shdepend","FullyQualifiedName":"localhost/postgres/pg_shdepend","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_shdepend","FullyQualifiedName":"localhost/postgres/pg_shdepend","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_shdepend","FullyQualifiedName":"localhost/postgres/pg_shdepend","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_shdepend","FullyQualifiedName":"localhost/postgres/pg_shdepend","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_shdepend","FullyQualifiedName":"localhost/postgres/pg_shdepend","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_shdescription","FullyQualifiedName":"localhost/postgres/pg_shdescription","Type":"table","Metadata":{"rows":"1","size":"64 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_shdescription","FullyQualifiedName":"localhost/postgres/pg_shdescription","Type":"table","Metadata":{"rows":"1","size":"64 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_shdescription","FullyQualifiedName":"localhost/postgres/pg_shdescription","Type":"table","Metadata":{"rows":"1","size":"64 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_shdescription","FullyQualifiedName":"localhost/postgres/pg_shdescription","Type":"table","Metadata":{"rows":"1","size":"64 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_shdescription","FullyQualifiedName":"localhost/postgres/pg_shdescription","Type":"table","Metadata":{"rows":"1","size":"64 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_shdescription","FullyQualifiedName":"localhost/postgres/pg_shdescription","Type":"table","Metadata":{"rows":"1","size":"64 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_shdescription","FullyQualifiedName":"localhost/postgres/pg_shdescription","Type":"table","Metadata":{"rows":"1","size":"64 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_shmem_allocations","FullyQualifiedName":"localhost/postgres/pg_shmem_allocations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_shmem_allocations","FullyQualifiedName":"localhost/postgres/pg_shmem_allocations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_shmem_allocations","FullyQualifiedName":"localhost/postgres/pg_shmem_allocations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_shmem_allocations","FullyQualifiedName":"localhost/postgres/pg_shmem_allocations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_shmem_allocations","FullyQualifiedName":"localhost/postgres/pg_shmem_allocations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_shmem_allocations","FullyQualifiedName":"localhost/postgres/pg_shmem_allocations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_shmem_allocations","FullyQualifiedName":"localhost/postgres/pg_shmem_allocations","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_shseclabel","FullyQualifiedName":"localhost/postgres/pg_shseclabel","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_shseclabel","FullyQualifiedName":"localhost/postgres/pg_shseclabel","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_shseclabel","FullyQualifiedName":"localhost/postgres/pg_shseclabel","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_shseclabel","FullyQualifiedName":"localhost/postgres/pg_shseclabel","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_shseclabel","FullyQualifiedName":"localhost/postgres/pg_shseclabel","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_shseclabel","FullyQualifiedName":"localhost/postgres/pg_shseclabel","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_shseclabel","FullyQualifiedName":"localhost/postgres/pg_shseclabel","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_activity","FullyQualifiedName":"localhost/postgres/pg_stat_activity","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_activity","FullyQualifiedName":"localhost/postgres/pg_stat_activity","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_activity","FullyQualifiedName":"localhost/postgres/pg_stat_activity","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_activity","FullyQualifiedName":"localhost/postgres/pg_stat_activity","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_activity","FullyQualifiedName":"localhost/postgres/pg_stat_activity","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_activity","FullyQualifiedName":"localhost/postgres/pg_stat_activity","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_activity","FullyQualifiedName":"localhost/postgres/pg_stat_activity","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_all_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_all_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_all_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_all_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_all_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_all_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_all_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_all_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_all_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_all_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_all_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_all_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_all_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_all_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_all_tables","FullyQualifiedName":"localhost/postgres/pg_stat_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_all_tables","FullyQualifiedName":"localhost/postgres/pg_stat_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_all_tables","FullyQualifiedName":"localhost/postgres/pg_stat_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_all_tables","FullyQualifiedName":"localhost/postgres/pg_stat_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_all_tables","FullyQualifiedName":"localhost/postgres/pg_stat_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_all_tables","FullyQualifiedName":"localhost/postgres/pg_stat_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_all_tables","FullyQualifiedName":"localhost/postgres/pg_stat_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_archiver","FullyQualifiedName":"localhost/postgres/pg_stat_archiver","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_archiver","FullyQualifiedName":"localhost/postgres/pg_stat_archiver","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_archiver","FullyQualifiedName":"localhost/postgres/pg_stat_archiver","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_archiver","FullyQualifiedName":"localhost/postgres/pg_stat_archiver","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_archiver","FullyQualifiedName":"localhost/postgres/pg_stat_archiver","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_archiver","FullyQualifiedName":"localhost/postgres/pg_stat_archiver","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_archiver","FullyQualifiedName":"localhost/postgres/pg_stat_archiver","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_bgwriter","FullyQualifiedName":"localhost/postgres/pg_stat_bgwriter","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_bgwriter","FullyQualifiedName":"localhost/postgres/pg_stat_bgwriter","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_bgwriter","FullyQualifiedName":"localhost/postgres/pg_stat_bgwriter","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_bgwriter","FullyQualifiedName":"localhost/postgres/pg_stat_bgwriter","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_bgwriter","FullyQualifiedName":"localhost/postgres/pg_stat_bgwriter","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_bgwriter","FullyQualifiedName":"localhost/postgres/pg_stat_bgwriter","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_bgwriter","FullyQualifiedName":"localhost/postgres/pg_stat_bgwriter","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_database","FullyQualifiedName":"localhost/postgres/pg_stat_database","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_database","FullyQualifiedName":"localhost/postgres/pg_stat_database","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_database","FullyQualifiedName":"localhost/postgres/pg_stat_database","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_database","FullyQualifiedName":"localhost/postgres/pg_stat_database","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_database","FullyQualifiedName":"localhost/postgres/pg_stat_database","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_database","FullyQualifiedName":"localhost/postgres/pg_stat_database","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_database","FullyQualifiedName":"localhost/postgres/pg_stat_database","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_database_conflicts","FullyQualifiedName":"localhost/postgres/pg_stat_database_conflicts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_database_conflicts","FullyQualifiedName":"localhost/postgres/pg_stat_database_conflicts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_database_conflicts","FullyQualifiedName":"localhost/postgres/pg_stat_database_conflicts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_database_conflicts","FullyQualifiedName":"localhost/postgres/pg_stat_database_conflicts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_database_conflicts","FullyQualifiedName":"localhost/postgres/pg_stat_database_conflicts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_database_conflicts","FullyQualifiedName":"localhost/postgres/pg_stat_database_conflicts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_database_conflicts","FullyQualifiedName":"localhost/postgres/pg_stat_database_conflicts","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_gssapi","FullyQualifiedName":"localhost/postgres/pg_stat_gssapi","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_gssapi","FullyQualifiedName":"localhost/postgres/pg_stat_gssapi","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_gssapi","FullyQualifiedName":"localhost/postgres/pg_stat_gssapi","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_gssapi","FullyQualifiedName":"localhost/postgres/pg_stat_gssapi","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_gssapi","FullyQualifiedName":"localhost/postgres/pg_stat_gssapi","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_gssapi","FullyQualifiedName":"localhost/postgres/pg_stat_gssapi","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_gssapi","FullyQualifiedName":"localhost/postgres/pg_stat_gssapi","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_io","FullyQualifiedName":"localhost/postgres/pg_stat_io","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_io","FullyQualifiedName":"localhost/postgres/pg_stat_io","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_io","FullyQualifiedName":"localhost/postgres/pg_stat_io","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_io","FullyQualifiedName":"localhost/postgres/pg_stat_io","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_io","FullyQualifiedName":"localhost/postgres/pg_stat_io","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_io","FullyQualifiedName":"localhost/postgres/pg_stat_io","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_io","FullyQualifiedName":"localhost/postgres/pg_stat_io","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_progress_analyze","FullyQualifiedName":"localhost/postgres/pg_stat_progress_analyze","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_progress_analyze","FullyQualifiedName":"localhost/postgres/pg_stat_progress_analyze","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_progress_analyze","FullyQualifiedName":"localhost/postgres/pg_stat_progress_analyze","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_progress_analyze","FullyQualifiedName":"localhost/postgres/pg_stat_progress_analyze","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_progress_analyze","FullyQualifiedName":"localhost/postgres/pg_stat_progress_analyze","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_progress_analyze","FullyQualifiedName":"localhost/postgres/pg_stat_progress_analyze","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_progress_analyze","FullyQualifiedName":"localhost/postgres/pg_stat_progress_analyze","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_progress_basebackup","FullyQualifiedName":"localhost/postgres/pg_stat_progress_basebackup","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_progress_basebackup","FullyQualifiedName":"localhost/postgres/pg_stat_progress_basebackup","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_progress_basebackup","FullyQualifiedName":"localhost/postgres/pg_stat_progress_basebackup","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_progress_basebackup","FullyQualifiedName":"localhost/postgres/pg_stat_progress_basebackup","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_progress_basebackup","FullyQualifiedName":"localhost/postgres/pg_stat_progress_basebackup","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_progress_basebackup","FullyQualifiedName":"localhost/postgres/pg_stat_progress_basebackup","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_progress_basebackup","FullyQualifiedName":"localhost/postgres/pg_stat_progress_basebackup","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_progress_cluster","FullyQualifiedName":"localhost/postgres/pg_stat_progress_cluster","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_progress_cluster","FullyQualifiedName":"localhost/postgres/pg_stat_progress_cluster","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_progress_cluster","FullyQualifiedName":"localhost/postgres/pg_stat_progress_cluster","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_progress_cluster","FullyQualifiedName":"localhost/postgres/pg_stat_progress_cluster","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_progress_cluster","FullyQualifiedName":"localhost/postgres/pg_stat_progress_cluster","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_progress_cluster","FullyQualifiedName":"localhost/postgres/pg_stat_progress_cluster","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_progress_cluster","FullyQualifiedName":"localhost/postgres/pg_stat_progress_cluster","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_progress_copy","FullyQualifiedName":"localhost/postgres/pg_stat_progress_copy","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_progress_copy","FullyQualifiedName":"localhost/postgres/pg_stat_progress_copy","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_progress_copy","FullyQualifiedName":"localhost/postgres/pg_stat_progress_copy","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_progress_copy","FullyQualifiedName":"localhost/postgres/pg_stat_progress_copy","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_progress_copy","FullyQualifiedName":"localhost/postgres/pg_stat_progress_copy","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_progress_copy","FullyQualifiedName":"localhost/postgres/pg_stat_progress_copy","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_progress_copy","FullyQualifiedName":"localhost/postgres/pg_stat_progress_copy","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_progress_create_index","FullyQualifiedName":"localhost/postgres/pg_stat_progress_create_index","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_progress_create_index","FullyQualifiedName":"localhost/postgres/pg_stat_progress_create_index","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_progress_create_index","FullyQualifiedName":"localhost/postgres/pg_stat_progress_create_index","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_progress_create_index","FullyQualifiedName":"localhost/postgres/pg_stat_progress_create_index","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_progress_create_index","FullyQualifiedName":"localhost/postgres/pg_stat_progress_create_index","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_progress_create_index","FullyQualifiedName":"localhost/postgres/pg_stat_progress_create_index","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_progress_create_index","FullyQualifiedName":"localhost/postgres/pg_stat_progress_create_index","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_progress_vacuum","FullyQualifiedName":"localhost/postgres/pg_stat_progress_vacuum","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_progress_vacuum","FullyQualifiedName":"localhost/postgres/pg_stat_progress_vacuum","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_progress_vacuum","FullyQualifiedName":"localhost/postgres/pg_stat_progress_vacuum","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_progress_vacuum","FullyQualifiedName":"localhost/postgres/pg_stat_progress_vacuum","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_progress_vacuum","FullyQualifiedName":"localhost/postgres/pg_stat_progress_vacuum","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_progress_vacuum","FullyQualifiedName":"localhost/postgres/pg_stat_progress_vacuum","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_progress_vacuum","FullyQualifiedName":"localhost/postgres/pg_stat_progress_vacuum","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_recovery_prefetch","FullyQualifiedName":"localhost/postgres/pg_stat_recovery_prefetch","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_recovery_prefetch","FullyQualifiedName":"localhost/postgres/pg_stat_recovery_prefetch","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_recovery_prefetch","FullyQualifiedName":"localhost/postgres/pg_stat_recovery_prefetch","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_recovery_prefetch","FullyQualifiedName":"localhost/postgres/pg_stat_recovery_prefetch","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_recovery_prefetch","FullyQualifiedName":"localhost/postgres/pg_stat_recovery_prefetch","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_recovery_prefetch","FullyQualifiedName":"localhost/postgres/pg_stat_recovery_prefetch","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_recovery_prefetch","FullyQualifiedName":"localhost/postgres/pg_stat_recovery_prefetch","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_replication","FullyQualifiedName":"localhost/postgres/pg_stat_replication","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_replication","FullyQualifiedName":"localhost/postgres/pg_stat_replication","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_replication","FullyQualifiedName":"localhost/postgres/pg_stat_replication","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_replication","FullyQualifiedName":"localhost/postgres/pg_stat_replication","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_replication","FullyQualifiedName":"localhost/postgres/pg_stat_replication","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_replication","FullyQualifiedName":"localhost/postgres/pg_stat_replication","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_replication","FullyQualifiedName":"localhost/postgres/pg_stat_replication","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_replication_slots","FullyQualifiedName":"localhost/postgres/pg_stat_replication_slots","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_replication_slots","FullyQualifiedName":"localhost/postgres/pg_stat_replication_slots","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_replication_slots","FullyQualifiedName":"localhost/postgres/pg_stat_replication_slots","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_replication_slots","FullyQualifiedName":"localhost/postgres/pg_stat_replication_slots","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_replication_slots","FullyQualifiedName":"localhost/postgres/pg_stat_replication_slots","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_replication_slots","FullyQualifiedName":"localhost/postgres/pg_stat_replication_slots","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_replication_slots","FullyQualifiedName":"localhost/postgres/pg_stat_replication_slots","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_slru","FullyQualifiedName":"localhost/postgres/pg_stat_slru","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_slru","FullyQualifiedName":"localhost/postgres/pg_stat_slru","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_slru","FullyQualifiedName":"localhost/postgres/pg_stat_slru","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_slru","FullyQualifiedName":"localhost/postgres/pg_stat_slru","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_slru","FullyQualifiedName":"localhost/postgres/pg_stat_slru","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_slru","FullyQualifiedName":"localhost/postgres/pg_stat_slru","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_slru","FullyQualifiedName":"localhost/postgres/pg_stat_slru","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_ssl","FullyQualifiedName":"localhost/postgres/pg_stat_ssl","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_ssl","FullyQualifiedName":"localhost/postgres/pg_stat_ssl","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_ssl","FullyQualifiedName":"localhost/postgres/pg_stat_ssl","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_ssl","FullyQualifiedName":"localhost/postgres/pg_stat_ssl","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_ssl","FullyQualifiedName":"localhost/postgres/pg_stat_ssl","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_ssl","FullyQualifiedName":"localhost/postgres/pg_stat_ssl","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_ssl","FullyQualifiedName":"localhost/postgres/pg_stat_ssl","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_subscription","FullyQualifiedName":"localhost/postgres/pg_stat_subscription","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_subscription","FullyQualifiedName":"localhost/postgres/pg_stat_subscription","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_subscription","FullyQualifiedName":"localhost/postgres/pg_stat_subscription","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_subscription","FullyQualifiedName":"localhost/postgres/pg_stat_subscription","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_subscription","FullyQualifiedName":"localhost/postgres/pg_stat_subscription","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_subscription","FullyQualifiedName":"localhost/postgres/pg_stat_subscription","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_subscription","FullyQualifiedName":"localhost/postgres/pg_stat_subscription","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_subscription_stats","FullyQualifiedName":"localhost/postgres/pg_stat_subscription_stats","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_subscription_stats","FullyQualifiedName":"localhost/postgres/pg_stat_subscription_stats","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_subscription_stats","FullyQualifiedName":"localhost/postgres/pg_stat_subscription_stats","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_subscription_stats","FullyQualifiedName":"localhost/postgres/pg_stat_subscription_stats","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_subscription_stats","FullyQualifiedName":"localhost/postgres/pg_stat_subscription_stats","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_subscription_stats","FullyQualifiedName":"localhost/postgres/pg_stat_subscription_stats","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_subscription_stats","FullyQualifiedName":"localhost/postgres/pg_stat_subscription_stats","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_sys_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_sys_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_sys_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_sys_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_sys_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_sys_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_sys_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_sys_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_sys_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_sys_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_sys_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_sys_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_sys_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_sys_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_sys_tables","FullyQualifiedName":"localhost/postgres/pg_stat_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_sys_tables","FullyQualifiedName":"localhost/postgres/pg_stat_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_sys_tables","FullyQualifiedName":"localhost/postgres/pg_stat_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_sys_tables","FullyQualifiedName":"localhost/postgres/pg_stat_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_sys_tables","FullyQualifiedName":"localhost/postgres/pg_stat_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_sys_tables","FullyQualifiedName":"localhost/postgres/pg_stat_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_sys_tables","FullyQualifiedName":"localhost/postgres/pg_stat_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_user_functions","FullyQualifiedName":"localhost/postgres/pg_stat_user_functions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_user_functions","FullyQualifiedName":"localhost/postgres/pg_stat_user_functions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_user_functions","FullyQualifiedName":"localhost/postgres/pg_stat_user_functions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_user_functions","FullyQualifiedName":"localhost/postgres/pg_stat_user_functions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_user_functions","FullyQualifiedName":"localhost/postgres/pg_stat_user_functions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_user_functions","FullyQualifiedName":"localhost/postgres/pg_stat_user_functions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_user_functions","FullyQualifiedName":"localhost/postgres/pg_stat_user_functions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_user_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_user_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_user_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_user_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_user_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_user_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_user_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_user_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_user_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_user_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_user_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_user_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_user_indexes","FullyQualifiedName":"localhost/postgres/pg_stat_user_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_user_tables","FullyQualifiedName":"localhost/postgres/pg_stat_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_user_tables","FullyQualifiedName":"localhost/postgres/pg_stat_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_user_tables","FullyQualifiedName":"localhost/postgres/pg_stat_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_user_tables","FullyQualifiedName":"localhost/postgres/pg_stat_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_user_tables","FullyQualifiedName":"localhost/postgres/pg_stat_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_user_tables","FullyQualifiedName":"localhost/postgres/pg_stat_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_user_tables","FullyQualifiedName":"localhost/postgres/pg_stat_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_wal","FullyQualifiedName":"localhost/postgres/pg_stat_wal","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_wal","FullyQualifiedName":"localhost/postgres/pg_stat_wal","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_wal","FullyQualifiedName":"localhost/postgres/pg_stat_wal","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_wal","FullyQualifiedName":"localhost/postgres/pg_stat_wal","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_wal","FullyQualifiedName":"localhost/postgres/pg_stat_wal","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_wal","FullyQualifiedName":"localhost/postgres/pg_stat_wal","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_wal","FullyQualifiedName":"localhost/postgres/pg_stat_wal","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_wal_receiver","FullyQualifiedName":"localhost/postgres/pg_stat_wal_receiver","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_wal_receiver","FullyQualifiedName":"localhost/postgres/pg_stat_wal_receiver","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_wal_receiver","FullyQualifiedName":"localhost/postgres/pg_stat_wal_receiver","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_wal_receiver","FullyQualifiedName":"localhost/postgres/pg_stat_wal_receiver","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_wal_receiver","FullyQualifiedName":"localhost/postgres/pg_stat_wal_receiver","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_wal_receiver","FullyQualifiedName":"localhost/postgres/pg_stat_wal_receiver","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_wal_receiver","FullyQualifiedName":"localhost/postgres/pg_stat_wal_receiver","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_xact_all_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_xact_all_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_xact_all_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_xact_all_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_xact_all_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_xact_all_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_xact_all_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_xact_sys_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_xact_sys_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_xact_sys_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_xact_sys_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_xact_sys_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_xact_sys_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_xact_sys_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_xact_user_functions","FullyQualifiedName":"localhost/postgres/pg_stat_xact_user_functions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_xact_user_functions","FullyQualifiedName":"localhost/postgres/pg_stat_xact_user_functions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_xact_user_functions","FullyQualifiedName":"localhost/postgres/pg_stat_xact_user_functions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_xact_user_functions","FullyQualifiedName":"localhost/postgres/pg_stat_xact_user_functions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_xact_user_functions","FullyQualifiedName":"localhost/postgres/pg_stat_xact_user_functions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_xact_user_functions","FullyQualifiedName":"localhost/postgres/pg_stat_xact_user_functions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_xact_user_functions","FullyQualifiedName":"localhost/postgres/pg_stat_xact_user_functions","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stat_xact_user_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stat_xact_user_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stat_xact_user_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stat_xact_user_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stat_xact_user_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stat_xact_user_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stat_xact_user_tables","FullyQualifiedName":"localhost/postgres/pg_stat_xact_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_statio_all_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_all_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_statio_all_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_all_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_statio_all_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_all_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_statio_all_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_all_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_statio_all_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_all_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_statio_all_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_all_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_statio_all_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_all_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_statio_all_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_all_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_statio_all_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_all_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_statio_all_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_all_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_statio_all_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_all_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_statio_all_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_all_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_statio_all_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_all_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_statio_all_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_all_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_statio_all_tables","FullyQualifiedName":"localhost/postgres/pg_statio_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_statio_all_tables","FullyQualifiedName":"localhost/postgres/pg_statio_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_statio_all_tables","FullyQualifiedName":"localhost/postgres/pg_statio_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_statio_all_tables","FullyQualifiedName":"localhost/postgres/pg_statio_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_statio_all_tables","FullyQualifiedName":"localhost/postgres/pg_statio_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_statio_all_tables","FullyQualifiedName":"localhost/postgres/pg_statio_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_statio_all_tables","FullyQualifiedName":"localhost/postgres/pg_statio_all_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_statio_sys_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_sys_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_statio_sys_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_sys_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_statio_sys_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_sys_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_statio_sys_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_sys_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_statio_sys_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_sys_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_statio_sys_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_sys_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_statio_sys_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_sys_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_statio_sys_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_sys_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_statio_sys_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_sys_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_statio_sys_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_sys_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_statio_sys_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_sys_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_statio_sys_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_sys_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_statio_sys_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_sys_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_statio_sys_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_sys_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_statio_sys_tables","FullyQualifiedName":"localhost/postgres/pg_statio_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_statio_sys_tables","FullyQualifiedName":"localhost/postgres/pg_statio_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_statio_sys_tables","FullyQualifiedName":"localhost/postgres/pg_statio_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_statio_sys_tables","FullyQualifiedName":"localhost/postgres/pg_statio_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_statio_sys_tables","FullyQualifiedName":"localhost/postgres/pg_statio_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_statio_sys_tables","FullyQualifiedName":"localhost/postgres/pg_statio_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_statio_sys_tables","FullyQualifiedName":"localhost/postgres/pg_statio_sys_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_statio_user_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_user_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_statio_user_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_user_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_statio_user_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_user_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_statio_user_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_user_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_statio_user_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_user_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_statio_user_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_user_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_statio_user_indexes","FullyQualifiedName":"localhost/postgres/pg_statio_user_indexes","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_statio_user_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_user_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_statio_user_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_user_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_statio_user_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_user_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_statio_user_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_user_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_statio_user_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_user_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_statio_user_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_user_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_statio_user_sequences","FullyQualifiedName":"localhost/postgres/pg_statio_user_sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_statio_user_tables","FullyQualifiedName":"localhost/postgres/pg_statio_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_statio_user_tables","FullyQualifiedName":"localhost/postgres/pg_statio_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_statio_user_tables","FullyQualifiedName":"localhost/postgres/pg_statio_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_statio_user_tables","FullyQualifiedName":"localhost/postgres/pg_statio_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_statio_user_tables","FullyQualifiedName":"localhost/postgres/pg_statio_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_statio_user_tables","FullyQualifiedName":"localhost/postgres/pg_statio_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_statio_user_tables","FullyQualifiedName":"localhost/postgres/pg_statio_user_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_statistic","FullyQualifiedName":"localhost/postgres/pg_statistic","Type":"table","Metadata":{"rows":"409","size":"288 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_statistic","FullyQualifiedName":"localhost/postgres/pg_statistic","Type":"table","Metadata":{"rows":"409","size":"288 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_statistic","FullyQualifiedName":"localhost/postgres/pg_statistic","Type":"table","Metadata":{"rows":"409","size":"288 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_statistic","FullyQualifiedName":"localhost/postgres/pg_statistic","Type":"table","Metadata":{"rows":"409","size":"288 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_statistic","FullyQualifiedName":"localhost/postgres/pg_statistic","Type":"table","Metadata":{"rows":"409","size":"288 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_statistic","FullyQualifiedName":"localhost/postgres/pg_statistic","Type":"table","Metadata":{"rows":"409","size":"288 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_statistic","FullyQualifiedName":"localhost/postgres/pg_statistic","Type":"table","Metadata":{"rows":"409","size":"288 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_statistic_ext","FullyQualifiedName":"localhost/postgres/pg_statistic_ext","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_statistic_ext","FullyQualifiedName":"localhost/postgres/pg_statistic_ext","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_statistic_ext","FullyQualifiedName":"localhost/postgres/pg_statistic_ext","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_statistic_ext","FullyQualifiedName":"localhost/postgres/pg_statistic_ext","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_statistic_ext","FullyQualifiedName":"localhost/postgres/pg_statistic_ext","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_statistic_ext","FullyQualifiedName":"localhost/postgres/pg_statistic_ext","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_statistic_ext","FullyQualifiedName":"localhost/postgres/pg_statistic_ext","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_statistic_ext_data","FullyQualifiedName":"localhost/postgres/pg_statistic_ext_data","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_statistic_ext_data","FullyQualifiedName":"localhost/postgres/pg_statistic_ext_data","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_statistic_ext_data","FullyQualifiedName":"localhost/postgres/pg_statistic_ext_data","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_statistic_ext_data","FullyQualifiedName":"localhost/postgres/pg_statistic_ext_data","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_statistic_ext_data","FullyQualifiedName":"localhost/postgres/pg_statistic_ext_data","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_statistic_ext_data","FullyQualifiedName":"localhost/postgres/pg_statistic_ext_data","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_statistic_ext_data","FullyQualifiedName":"localhost/postgres/pg_statistic_ext_data","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stats","FullyQualifiedName":"localhost/postgres/pg_stats","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stats","FullyQualifiedName":"localhost/postgres/pg_stats","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stats","FullyQualifiedName":"localhost/postgres/pg_stats","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stats","FullyQualifiedName":"localhost/postgres/pg_stats","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stats","FullyQualifiedName":"localhost/postgres/pg_stats","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stats","FullyQualifiedName":"localhost/postgres/pg_stats","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stats","FullyQualifiedName":"localhost/postgres/pg_stats","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stats_ext","FullyQualifiedName":"localhost/postgres/pg_stats_ext","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stats_ext","FullyQualifiedName":"localhost/postgres/pg_stats_ext","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stats_ext","FullyQualifiedName":"localhost/postgres/pg_stats_ext","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stats_ext","FullyQualifiedName":"localhost/postgres/pg_stats_ext","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stats_ext","FullyQualifiedName":"localhost/postgres/pg_stats_ext","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stats_ext","FullyQualifiedName":"localhost/postgres/pg_stats_ext","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stats_ext","FullyQualifiedName":"localhost/postgres/pg_stats_ext","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_stats_ext_exprs","FullyQualifiedName":"localhost/postgres/pg_stats_ext_exprs","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_stats_ext_exprs","FullyQualifiedName":"localhost/postgres/pg_stats_ext_exprs","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_stats_ext_exprs","FullyQualifiedName":"localhost/postgres/pg_stats_ext_exprs","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_stats_ext_exprs","FullyQualifiedName":"localhost/postgres/pg_stats_ext_exprs","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_stats_ext_exprs","FullyQualifiedName":"localhost/postgres/pg_stats_ext_exprs","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_stats_ext_exprs","FullyQualifiedName":"localhost/postgres/pg_stats_ext_exprs","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_stats_ext_exprs","FullyQualifiedName":"localhost/postgres/pg_stats_ext_exprs","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_subscription","FullyQualifiedName":"localhost/postgres/pg_subscription","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_subscription","FullyQualifiedName":"localhost/postgres/pg_subscription","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_subscription","FullyQualifiedName":"localhost/postgres/pg_subscription","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_subscription","FullyQualifiedName":"localhost/postgres/pg_subscription","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_subscription","FullyQualifiedName":"localhost/postgres/pg_subscription","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_subscription","FullyQualifiedName":"localhost/postgres/pg_subscription","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_subscription","FullyQualifiedName":"localhost/postgres/pg_subscription","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_subscription_rel","FullyQualifiedName":"localhost/postgres/pg_subscription_rel","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_subscription_rel","FullyQualifiedName":"localhost/postgres/pg_subscription_rel","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_subscription_rel","FullyQualifiedName":"localhost/postgres/pg_subscription_rel","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_subscription_rel","FullyQualifiedName":"localhost/postgres/pg_subscription_rel","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_subscription_rel","FullyQualifiedName":"localhost/postgres/pg_subscription_rel","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_subscription_rel","FullyQualifiedName":"localhost/postgres/pg_subscription_rel","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_subscription_rel","FullyQualifiedName":"localhost/postgres/pg_subscription_rel","Type":"table","Metadata":{"rows":"0","size":"8192 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_tables","FullyQualifiedName":"localhost/postgres/pg_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_tables","FullyQualifiedName":"localhost/postgres/pg_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_tables","FullyQualifiedName":"localhost/postgres/pg_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_tables","FullyQualifiedName":"localhost/postgres/pg_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_tables","FullyQualifiedName":"localhost/postgres/pg_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_tables","FullyQualifiedName":"localhost/postgres/pg_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_tables","FullyQualifiedName":"localhost/postgres/pg_tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_tablespace","FullyQualifiedName":"localhost/postgres/pg_tablespace","Type":"table","Metadata":{"rows":"2","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_tablespace","FullyQualifiedName":"localhost/postgres/pg_tablespace","Type":"table","Metadata":{"rows":"2","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_tablespace","FullyQualifiedName":"localhost/postgres/pg_tablespace","Type":"table","Metadata":{"rows":"2","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_tablespace","FullyQualifiedName":"localhost/postgres/pg_tablespace","Type":"table","Metadata":{"rows":"2","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_tablespace","FullyQualifiedName":"localhost/postgres/pg_tablespace","Type":"table","Metadata":{"rows":"2","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_tablespace","FullyQualifiedName":"localhost/postgres/pg_tablespace","Type":"table","Metadata":{"rows":"2","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_tablespace","FullyQualifiedName":"localhost/postgres/pg_tablespace","Type":"table","Metadata":{"rows":"2","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_timezone_abbrevs","FullyQualifiedName":"localhost/postgres/pg_timezone_abbrevs","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_timezone_abbrevs","FullyQualifiedName":"localhost/postgres/pg_timezone_abbrevs","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_timezone_abbrevs","FullyQualifiedName":"localhost/postgres/pg_timezone_abbrevs","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_timezone_abbrevs","FullyQualifiedName":"localhost/postgres/pg_timezone_abbrevs","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_timezone_abbrevs","FullyQualifiedName":"localhost/postgres/pg_timezone_abbrevs","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_timezone_abbrevs","FullyQualifiedName":"localhost/postgres/pg_timezone_abbrevs","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_timezone_abbrevs","FullyQualifiedName":"localhost/postgres/pg_timezone_abbrevs","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_timezone_names","FullyQualifiedName":"localhost/postgres/pg_timezone_names","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_timezone_names","FullyQualifiedName":"localhost/postgres/pg_timezone_names","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_timezone_names","FullyQualifiedName":"localhost/postgres/pg_timezone_names","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_timezone_names","FullyQualifiedName":"localhost/postgres/pg_timezone_names","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_timezone_names","FullyQualifiedName":"localhost/postgres/pg_timezone_names","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_timezone_names","FullyQualifiedName":"localhost/postgres/pg_timezone_names","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_timezone_names","FullyQualifiedName":"localhost/postgres/pg_timezone_names","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_transform","FullyQualifiedName":"localhost/postgres/pg_transform","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_transform","FullyQualifiedName":"localhost/postgres/pg_transform","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_transform","FullyQualifiedName":"localhost/postgres/pg_transform","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_transform","FullyQualifiedName":"localhost/postgres/pg_transform","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_transform","FullyQualifiedName":"localhost/postgres/pg_transform","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_transform","FullyQualifiedName":"localhost/postgres/pg_transform","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_transform","FullyQualifiedName":"localhost/postgres/pg_transform","Type":"table","Metadata":{"rows":"0","size":"16 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_trigger","FullyQualifiedName":"localhost/postgres/pg_trigger","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_trigger","FullyQualifiedName":"localhost/postgres/pg_trigger","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_trigger","FullyQualifiedName":"localhost/postgres/pg_trigger","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_trigger","FullyQualifiedName":"localhost/postgres/pg_trigger","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_trigger","FullyQualifiedName":"localhost/postgres/pg_trigger","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_trigger","FullyQualifiedName":"localhost/postgres/pg_trigger","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_trigger","FullyQualifiedName":"localhost/postgres/pg_trigger","Type":"table","Metadata":{"rows":"0","size":"32 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_ts_config","FullyQualifiedName":"localhost/postgres/pg_ts_config","Type":"table","Metadata":{"rows":"29","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_ts_config","FullyQualifiedName":"localhost/postgres/pg_ts_config","Type":"table","Metadata":{"rows":"29","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_ts_config","FullyQualifiedName":"localhost/postgres/pg_ts_config","Type":"table","Metadata":{"rows":"29","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_ts_config","FullyQualifiedName":"localhost/postgres/pg_ts_config","Type":"table","Metadata":{"rows":"29","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_ts_config","FullyQualifiedName":"localhost/postgres/pg_ts_config","Type":"table","Metadata":{"rows":"29","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_ts_config","FullyQualifiedName":"localhost/postgres/pg_ts_config","Type":"table","Metadata":{"rows":"29","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_ts_config","FullyQualifiedName":"localhost/postgres/pg_ts_config","Type":"table","Metadata":{"rows":"29","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_ts_config_map","FullyQualifiedName":"localhost/postgres/pg_ts_config_map","Type":"table","Metadata":{"rows":"551","size":"88 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_ts_config_map","FullyQualifiedName":"localhost/postgres/pg_ts_config_map","Type":"table","Metadata":{"rows":"551","size":"88 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_ts_config_map","FullyQualifiedName":"localhost/postgres/pg_ts_config_map","Type":"table","Metadata":{"rows":"551","size":"88 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_ts_config_map","FullyQualifiedName":"localhost/postgres/pg_ts_config_map","Type":"table","Metadata":{"rows":"551","size":"88 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_ts_config_map","FullyQualifiedName":"localhost/postgres/pg_ts_config_map","Type":"table","Metadata":{"rows":"551","size":"88 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_ts_config_map","FullyQualifiedName":"localhost/postgres/pg_ts_config_map","Type":"table","Metadata":{"rows":"551","size":"88 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_ts_config_map","FullyQualifiedName":"localhost/postgres/pg_ts_config_map","Type":"table","Metadata":{"rows":"551","size":"88 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_ts_dict","FullyQualifiedName":"localhost/postgres/pg_ts_dict","Type":"table","Metadata":{"rows":"29","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_ts_dict","FullyQualifiedName":"localhost/postgres/pg_ts_dict","Type":"table","Metadata":{"rows":"29","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_ts_dict","FullyQualifiedName":"localhost/postgres/pg_ts_dict","Type":"table","Metadata":{"rows":"29","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_ts_dict","FullyQualifiedName":"localhost/postgres/pg_ts_dict","Type":"table","Metadata":{"rows":"29","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_ts_dict","FullyQualifiedName":"localhost/postgres/pg_ts_dict","Type":"table","Metadata":{"rows":"29","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_ts_dict","FullyQualifiedName":"localhost/postgres/pg_ts_dict","Type":"table","Metadata":{"rows":"29","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_ts_dict","FullyQualifiedName":"localhost/postgres/pg_ts_dict","Type":"table","Metadata":{"rows":"29","size":"80 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_ts_parser","FullyQualifiedName":"localhost/postgres/pg_ts_parser","Type":"table","Metadata":{"rows":"1","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_ts_parser","FullyQualifiedName":"localhost/postgres/pg_ts_parser","Type":"table","Metadata":{"rows":"1","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_ts_parser","FullyQualifiedName":"localhost/postgres/pg_ts_parser","Type":"table","Metadata":{"rows":"1","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_ts_parser","FullyQualifiedName":"localhost/postgres/pg_ts_parser","Type":"table","Metadata":{"rows":"1","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_ts_parser","FullyQualifiedName":"localhost/postgres/pg_ts_parser","Type":"table","Metadata":{"rows":"1","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_ts_parser","FullyQualifiedName":"localhost/postgres/pg_ts_parser","Type":"table","Metadata":{"rows":"1","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_ts_parser","FullyQualifiedName":"localhost/postgres/pg_ts_parser","Type":"table","Metadata":{"rows":"1","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_ts_template","FullyQualifiedName":"localhost/postgres/pg_ts_template","Type":"table","Metadata":{"rows":"5","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_ts_template","FullyQualifiedName":"localhost/postgres/pg_ts_template","Type":"table","Metadata":{"rows":"5","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_ts_template","FullyQualifiedName":"localhost/postgres/pg_ts_template","Type":"table","Metadata":{"rows":"5","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_ts_template","FullyQualifiedName":"localhost/postgres/pg_ts_template","Type":"table","Metadata":{"rows":"5","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_ts_template","FullyQualifiedName":"localhost/postgres/pg_ts_template","Type":"table","Metadata":{"rows":"5","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_ts_template","FullyQualifiedName":"localhost/postgres/pg_ts_template","Type":"table","Metadata":{"rows":"5","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_ts_template","FullyQualifiedName":"localhost/postgres/pg_ts_template","Type":"table","Metadata":{"rows":"5","size":"72 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_type","FullyQualifiedName":"localhost/postgres/pg_type","Type":"table","Metadata":{"rows":"613","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_type","FullyQualifiedName":"localhost/postgres/pg_type","Type":"table","Metadata":{"rows":"613","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_type","FullyQualifiedName":"localhost/postgres/pg_type","Type":"table","Metadata":{"rows":"613","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_type","FullyQualifiedName":"localhost/postgres/pg_type","Type":"table","Metadata":{"rows":"613","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_type","FullyQualifiedName":"localhost/postgres/pg_type","Type":"table","Metadata":{"rows":"613","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_type","FullyQualifiedName":"localhost/postgres/pg_type","Type":"table","Metadata":{"rows":"613","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_type","FullyQualifiedName":"localhost/postgres/pg_type","Type":"table","Metadata":{"rows":"613","size":"232 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_user","FullyQualifiedName":"localhost/postgres/pg_user","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_user","FullyQualifiedName":"localhost/postgres/pg_user","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_user","FullyQualifiedName":"localhost/postgres/pg_user","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_user","FullyQualifiedName":"localhost/postgres/pg_user","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_user","FullyQualifiedName":"localhost/postgres/pg_user","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_user","FullyQualifiedName":"localhost/postgres/pg_user","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_user","FullyQualifiedName":"localhost/postgres/pg_user","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_user_mapping","FullyQualifiedName":"localhost/postgres/pg_user_mapping","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_user_mapping","FullyQualifiedName":"localhost/postgres/pg_user_mapping","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_user_mapping","FullyQualifiedName":"localhost/postgres/pg_user_mapping","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_user_mapping","FullyQualifiedName":"localhost/postgres/pg_user_mapping","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_user_mapping","FullyQualifiedName":"localhost/postgres/pg_user_mapping","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_user_mapping","FullyQualifiedName":"localhost/postgres/pg_user_mapping","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_user_mapping","FullyQualifiedName":"localhost/postgres/pg_user_mapping","Type":"table","Metadata":{"rows":"0","size":"24 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_user_mappings","FullyQualifiedName":"localhost/postgres/pg_user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_user_mappings","FullyQualifiedName":"localhost/postgres/pg_user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_user_mappings","FullyQualifiedName":"localhost/postgres/pg_user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_user_mappings","FullyQualifiedName":"localhost/postgres/pg_user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_user_mappings","FullyQualifiedName":"localhost/postgres/pg_user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_user_mappings","FullyQualifiedName":"localhost/postgres/pg_user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_user_mappings","FullyQualifiedName":"localhost/postgres/pg_user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"pg_views","FullyQualifiedName":"localhost/postgres/pg_views","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"pg_views","FullyQualifiedName":"localhost/postgres/pg_views","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"pg_views","FullyQualifiedName":"localhost/postgres/pg_views","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"pg_views","FullyQualifiedName":"localhost/postgres/pg_views","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"pg_views","FullyQualifiedName":"localhost/postgres/pg_views","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"pg_views","FullyQualifiedName":"localhost/postgres/pg_views","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"pg_views","FullyQualifiedName":"localhost/postgres/pg_views","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null},"Permission":{"Value":"Bypass RLS","Parent":null}},{"Resource":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null},"Permission":{"Value":"Create DB","Parent":null}},{"Resource":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null},"Permission":{"Value":"Create Role","Parent":null}},{"Resource":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null},"Permission":{"Value":"Inheritance of Privs","Parent":null}},{"Resource":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null},"Permission":{"Value":"Login","Parent":null}},{"Resource":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null},"Permission":{"Value":"Replication","Parent":null}},{"Resource":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null},"Permission":{"Value":"Superuser","Parent":null}},{"Resource":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}},"Permission":{"Value":"connect","Parent":null}},{"Resource":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}},"Permission":{"Value":"create","Parent":null}},{"Resource":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}},"Permission":{"Value":"temp","Parent":null}},{"Resource":{"Name":"referential_constraints","FullyQualifiedName":"localhost/postgres/referential_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"referential_constraints","FullyQualifiedName":"localhost/postgres/referential_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"referential_constraints","FullyQualifiedName":"localhost/postgres/referential_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"referential_constraints","FullyQualifiedName":"localhost/postgres/referential_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"referential_constraints","FullyQualifiedName":"localhost/postgres/referential_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"referential_constraints","FullyQualifiedName":"localhost/postgres/referential_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"referential_constraints","FullyQualifiedName":"localhost/postgres/referential_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"role_column_grants","FullyQualifiedName":"localhost/postgres/role_column_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"role_column_grants","FullyQualifiedName":"localhost/postgres/role_column_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"role_column_grants","FullyQualifiedName":"localhost/postgres/role_column_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"role_column_grants","FullyQualifiedName":"localhost/postgres/role_column_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"role_column_grants","FullyQualifiedName":"localhost/postgres/role_column_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"role_column_grants","FullyQualifiedName":"localhost/postgres/role_column_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"role_column_grants","FullyQualifiedName":"localhost/postgres/role_column_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"role_routine_grants","FullyQualifiedName":"localhost/postgres/role_routine_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"role_routine_grants","FullyQualifiedName":"localhost/postgres/role_routine_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"role_routine_grants","FullyQualifiedName":"localhost/postgres/role_routine_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"role_routine_grants","FullyQualifiedName":"localhost/postgres/role_routine_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"role_routine_grants","FullyQualifiedName":"localhost/postgres/role_routine_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"role_routine_grants","FullyQualifiedName":"localhost/postgres/role_routine_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"role_routine_grants","FullyQualifiedName":"localhost/postgres/role_routine_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"role_table_grants","FullyQualifiedName":"localhost/postgres/role_table_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"role_table_grants","FullyQualifiedName":"localhost/postgres/role_table_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"role_table_grants","FullyQualifiedName":"localhost/postgres/role_table_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"role_table_grants","FullyQualifiedName":"localhost/postgres/role_table_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"role_table_grants","FullyQualifiedName":"localhost/postgres/role_table_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"role_table_grants","FullyQualifiedName":"localhost/postgres/role_table_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"role_table_grants","FullyQualifiedName":"localhost/postgres/role_table_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"role_udt_grants","FullyQualifiedName":"localhost/postgres/role_udt_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"role_udt_grants","FullyQualifiedName":"localhost/postgres/role_udt_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"role_udt_grants","FullyQualifiedName":"localhost/postgres/role_udt_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"role_udt_grants","FullyQualifiedName":"localhost/postgres/role_udt_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"role_udt_grants","FullyQualifiedName":"localhost/postgres/role_udt_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"role_udt_grants","FullyQualifiedName":"localhost/postgres/role_udt_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"role_udt_grants","FullyQualifiedName":"localhost/postgres/role_udt_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"role_usage_grants","FullyQualifiedName":"localhost/postgres/role_usage_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"role_usage_grants","FullyQualifiedName":"localhost/postgres/role_usage_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"role_usage_grants","FullyQualifiedName":"localhost/postgres/role_usage_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"role_usage_grants","FullyQualifiedName":"localhost/postgres/role_usage_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"role_usage_grants","FullyQualifiedName":"localhost/postgres/role_usage_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"role_usage_grants","FullyQualifiedName":"localhost/postgres/role_usage_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"role_usage_grants","FullyQualifiedName":"localhost/postgres/role_usage_grants","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"routine_column_usage","FullyQualifiedName":"localhost/postgres/routine_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"routine_column_usage","FullyQualifiedName":"localhost/postgres/routine_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"routine_column_usage","FullyQualifiedName":"localhost/postgres/routine_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"routine_column_usage","FullyQualifiedName":"localhost/postgres/routine_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"routine_column_usage","FullyQualifiedName":"localhost/postgres/routine_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"routine_column_usage","FullyQualifiedName":"localhost/postgres/routine_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"routine_column_usage","FullyQualifiedName":"localhost/postgres/routine_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"routine_privileges","FullyQualifiedName":"localhost/postgres/routine_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"routine_privileges","FullyQualifiedName":"localhost/postgres/routine_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"routine_privileges","FullyQualifiedName":"localhost/postgres/routine_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"routine_privileges","FullyQualifiedName":"localhost/postgres/routine_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"routine_privileges","FullyQualifiedName":"localhost/postgres/routine_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"routine_privileges","FullyQualifiedName":"localhost/postgres/routine_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"routine_privileges","FullyQualifiedName":"localhost/postgres/routine_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"routine_routine_usage","FullyQualifiedName":"localhost/postgres/routine_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"routine_routine_usage","FullyQualifiedName":"localhost/postgres/routine_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"routine_routine_usage","FullyQualifiedName":"localhost/postgres/routine_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"routine_routine_usage","FullyQualifiedName":"localhost/postgres/routine_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"routine_routine_usage","FullyQualifiedName":"localhost/postgres/routine_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"routine_routine_usage","FullyQualifiedName":"localhost/postgres/routine_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"routine_routine_usage","FullyQualifiedName":"localhost/postgres/routine_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"routine_sequence_usage","FullyQualifiedName":"localhost/postgres/routine_sequence_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"routine_sequence_usage","FullyQualifiedName":"localhost/postgres/routine_sequence_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"routine_sequence_usage","FullyQualifiedName":"localhost/postgres/routine_sequence_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"routine_sequence_usage","FullyQualifiedName":"localhost/postgres/routine_sequence_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"routine_sequence_usage","FullyQualifiedName":"localhost/postgres/routine_sequence_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"routine_sequence_usage","FullyQualifiedName":"localhost/postgres/routine_sequence_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"routine_sequence_usage","FullyQualifiedName":"localhost/postgres/routine_sequence_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"routine_table_usage","FullyQualifiedName":"localhost/postgres/routine_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"routine_table_usage","FullyQualifiedName":"localhost/postgres/routine_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"routine_table_usage","FullyQualifiedName":"localhost/postgres/routine_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"routine_table_usage","FullyQualifiedName":"localhost/postgres/routine_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"routine_table_usage","FullyQualifiedName":"localhost/postgres/routine_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"routine_table_usage","FullyQualifiedName":"localhost/postgres/routine_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"routine_table_usage","FullyQualifiedName":"localhost/postgres/routine_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"routines","FullyQualifiedName":"localhost/postgres/routines","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"routines","FullyQualifiedName":"localhost/postgres/routines","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"routines","FullyQualifiedName":"localhost/postgres/routines","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"routines","FullyQualifiedName":"localhost/postgres/routines","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"routines","FullyQualifiedName":"localhost/postgres/routines","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"routines","FullyQualifiedName":"localhost/postgres/routines","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"routines","FullyQualifiedName":"localhost/postgres/routines","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"schemata","FullyQualifiedName":"localhost/postgres/schemata","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"schemata","FullyQualifiedName":"localhost/postgres/schemata","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"schemata","FullyQualifiedName":"localhost/postgres/schemata","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"schemata","FullyQualifiedName":"localhost/postgres/schemata","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"schemata","FullyQualifiedName":"localhost/postgres/schemata","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"schemata","FullyQualifiedName":"localhost/postgres/schemata","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"schemata","FullyQualifiedName":"localhost/postgres/schemata","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"sequences","FullyQualifiedName":"localhost/postgres/sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"sequences","FullyQualifiedName":"localhost/postgres/sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"sequences","FullyQualifiedName":"localhost/postgres/sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"sequences","FullyQualifiedName":"localhost/postgres/sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"sequences","FullyQualifiedName":"localhost/postgres/sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"sequences","FullyQualifiedName":"localhost/postgres/sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"sequences","FullyQualifiedName":"localhost/postgres/sequences","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"sql_features","FullyQualifiedName":"localhost/postgres/sql_features","Type":"table","Metadata":{"rows":"755","size":"104 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"sql_features","FullyQualifiedName":"localhost/postgres/sql_features","Type":"table","Metadata":{"rows":"755","size":"104 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"sql_features","FullyQualifiedName":"localhost/postgres/sql_features","Type":"table","Metadata":{"rows":"755","size":"104 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"sql_features","FullyQualifiedName":"localhost/postgres/sql_features","Type":"table","Metadata":{"rows":"755","size":"104 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"sql_features","FullyQualifiedName":"localhost/postgres/sql_features","Type":"table","Metadata":{"rows":"755","size":"104 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"sql_features","FullyQualifiedName":"localhost/postgres/sql_features","Type":"table","Metadata":{"rows":"755","size":"104 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"sql_features","FullyQualifiedName":"localhost/postgres/sql_features","Type":"table","Metadata":{"rows":"755","size":"104 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"sql_implementation_info","FullyQualifiedName":"localhost/postgres/sql_implementation_info","Type":"table","Metadata":{"rows":"12","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"sql_implementation_info","FullyQualifiedName":"localhost/postgres/sql_implementation_info","Type":"table","Metadata":{"rows":"12","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"sql_implementation_info","FullyQualifiedName":"localhost/postgres/sql_implementation_info","Type":"table","Metadata":{"rows":"12","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"sql_implementation_info","FullyQualifiedName":"localhost/postgres/sql_implementation_info","Type":"table","Metadata":{"rows":"12","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"sql_implementation_info","FullyQualifiedName":"localhost/postgres/sql_implementation_info","Type":"table","Metadata":{"rows":"12","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"sql_implementation_info","FullyQualifiedName":"localhost/postgres/sql_implementation_info","Type":"table","Metadata":{"rows":"12","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"sql_implementation_info","FullyQualifiedName":"localhost/postgres/sql_implementation_info","Type":"table","Metadata":{"rows":"12","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"sql_parts","FullyQualifiedName":"localhost/postgres/sql_parts","Type":"table","Metadata":{"rows":"11","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"sql_parts","FullyQualifiedName":"localhost/postgres/sql_parts","Type":"table","Metadata":{"rows":"11","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"sql_parts","FullyQualifiedName":"localhost/postgres/sql_parts","Type":"table","Metadata":{"rows":"11","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"sql_parts","FullyQualifiedName":"localhost/postgres/sql_parts","Type":"table","Metadata":{"rows":"11","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"sql_parts","FullyQualifiedName":"localhost/postgres/sql_parts","Type":"table","Metadata":{"rows":"11","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"sql_parts","FullyQualifiedName":"localhost/postgres/sql_parts","Type":"table","Metadata":{"rows":"11","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"sql_parts","FullyQualifiedName":"localhost/postgres/sql_parts","Type":"table","Metadata":{"rows":"11","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"sql_sizing","FullyQualifiedName":"localhost/postgres/sql_sizing","Type":"table","Metadata":{"rows":"23","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"sql_sizing","FullyQualifiedName":"localhost/postgres/sql_sizing","Type":"table","Metadata":{"rows":"23","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"sql_sizing","FullyQualifiedName":"localhost/postgres/sql_sizing","Type":"table","Metadata":{"rows":"23","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"sql_sizing","FullyQualifiedName":"localhost/postgres/sql_sizing","Type":"table","Metadata":{"rows":"23","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"sql_sizing","FullyQualifiedName":"localhost/postgres/sql_sizing","Type":"table","Metadata":{"rows":"23","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"sql_sizing","FullyQualifiedName":"localhost/postgres/sql_sizing","Type":"table","Metadata":{"rows":"23","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"sql_sizing","FullyQualifiedName":"localhost/postgres/sql_sizing","Type":"table","Metadata":{"rows":"23","size":"48 kB"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"table_constraints","FullyQualifiedName":"localhost/postgres/table_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"table_constraints","FullyQualifiedName":"localhost/postgres/table_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"table_constraints","FullyQualifiedName":"localhost/postgres/table_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"table_constraints","FullyQualifiedName":"localhost/postgres/table_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"table_constraints","FullyQualifiedName":"localhost/postgres/table_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"table_constraints","FullyQualifiedName":"localhost/postgres/table_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"table_constraints","FullyQualifiedName":"localhost/postgres/table_constraints","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"table_privileges","FullyQualifiedName":"localhost/postgres/table_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"table_privileges","FullyQualifiedName":"localhost/postgres/table_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"table_privileges","FullyQualifiedName":"localhost/postgres/table_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"table_privileges","FullyQualifiedName":"localhost/postgres/table_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"table_privileges","FullyQualifiedName":"localhost/postgres/table_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"table_privileges","FullyQualifiedName":"localhost/postgres/table_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"table_privileges","FullyQualifiedName":"localhost/postgres/table_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"tables","FullyQualifiedName":"localhost/postgres/tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"tables","FullyQualifiedName":"localhost/postgres/tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"tables","FullyQualifiedName":"localhost/postgres/tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"tables","FullyQualifiedName":"localhost/postgres/tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"tables","FullyQualifiedName":"localhost/postgres/tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"tables","FullyQualifiedName":"localhost/postgres/tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"tables","FullyQualifiedName":"localhost/postgres/tables","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"transforms","FullyQualifiedName":"localhost/postgres/transforms","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"transforms","FullyQualifiedName":"localhost/postgres/transforms","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"transforms","FullyQualifiedName":"localhost/postgres/transforms","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"transforms","FullyQualifiedName":"localhost/postgres/transforms","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"transforms","FullyQualifiedName":"localhost/postgres/transforms","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"transforms","FullyQualifiedName":"localhost/postgres/transforms","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"transforms","FullyQualifiedName":"localhost/postgres/transforms","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"triggered_update_columns","FullyQualifiedName":"localhost/postgres/triggered_update_columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"triggered_update_columns","FullyQualifiedName":"localhost/postgres/triggered_update_columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"triggered_update_columns","FullyQualifiedName":"localhost/postgres/triggered_update_columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"triggered_update_columns","FullyQualifiedName":"localhost/postgres/triggered_update_columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"triggered_update_columns","FullyQualifiedName":"localhost/postgres/triggered_update_columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"triggered_update_columns","FullyQualifiedName":"localhost/postgres/triggered_update_columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"triggered_update_columns","FullyQualifiedName":"localhost/postgres/triggered_update_columns","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"triggers","FullyQualifiedName":"localhost/postgres/triggers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"triggers","FullyQualifiedName":"localhost/postgres/triggers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"triggers","FullyQualifiedName":"localhost/postgres/triggers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"triggers","FullyQualifiedName":"localhost/postgres/triggers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"triggers","FullyQualifiedName":"localhost/postgres/triggers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"triggers","FullyQualifiedName":"localhost/postgres/triggers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"triggers","FullyQualifiedName":"localhost/postgres/triggers","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"udt_privileges","FullyQualifiedName":"localhost/postgres/udt_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"udt_privileges","FullyQualifiedName":"localhost/postgres/udt_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"udt_privileges","FullyQualifiedName":"localhost/postgres/udt_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"udt_privileges","FullyQualifiedName":"localhost/postgres/udt_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"udt_privileges","FullyQualifiedName":"localhost/postgres/udt_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"udt_privileges","FullyQualifiedName":"localhost/postgres/udt_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"udt_privileges","FullyQualifiedName":"localhost/postgres/udt_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"usage_privileges","FullyQualifiedName":"localhost/postgres/usage_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"usage_privileges","FullyQualifiedName":"localhost/postgres/usage_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"usage_privileges","FullyQualifiedName":"localhost/postgres/usage_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"usage_privileges","FullyQualifiedName":"localhost/postgres/usage_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"usage_privileges","FullyQualifiedName":"localhost/postgres/usage_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"usage_privileges","FullyQualifiedName":"localhost/postgres/usage_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"usage_privileges","FullyQualifiedName":"localhost/postgres/usage_privileges","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"user_defined_types","FullyQualifiedName":"localhost/postgres/user_defined_types","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"user_defined_types","FullyQualifiedName":"localhost/postgres/user_defined_types","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"user_defined_types","FullyQualifiedName":"localhost/postgres/user_defined_types","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"user_defined_types","FullyQualifiedName":"localhost/postgres/user_defined_types","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"user_defined_types","FullyQualifiedName":"localhost/postgres/user_defined_types","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"user_defined_types","FullyQualifiedName":"localhost/postgres/user_defined_types","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"user_defined_types","FullyQualifiedName":"localhost/postgres/user_defined_types","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"user_mapping_options","FullyQualifiedName":"localhost/postgres/user_mapping_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"user_mapping_options","FullyQualifiedName":"localhost/postgres/user_mapping_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"user_mapping_options","FullyQualifiedName":"localhost/postgres/user_mapping_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"user_mapping_options","FullyQualifiedName":"localhost/postgres/user_mapping_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"user_mapping_options","FullyQualifiedName":"localhost/postgres/user_mapping_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"user_mapping_options","FullyQualifiedName":"localhost/postgres/user_mapping_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"user_mapping_options","FullyQualifiedName":"localhost/postgres/user_mapping_options","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"user_mappings","FullyQualifiedName":"localhost/postgres/user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"user_mappings","FullyQualifiedName":"localhost/postgres/user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"user_mappings","FullyQualifiedName":"localhost/postgres/user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"user_mappings","FullyQualifiedName":"localhost/postgres/user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"user_mappings","FullyQualifiedName":"localhost/postgres/user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"user_mappings","FullyQualifiedName":"localhost/postgres/user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"user_mappings","FullyQualifiedName":"localhost/postgres/user_mappings","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"view_column_usage","FullyQualifiedName":"localhost/postgres/view_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"view_column_usage","FullyQualifiedName":"localhost/postgres/view_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"view_column_usage","FullyQualifiedName":"localhost/postgres/view_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"view_column_usage","FullyQualifiedName":"localhost/postgres/view_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"view_column_usage","FullyQualifiedName":"localhost/postgres/view_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"view_column_usage","FullyQualifiedName":"localhost/postgres/view_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"view_column_usage","FullyQualifiedName":"localhost/postgres/view_column_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"view_routine_usage","FullyQualifiedName":"localhost/postgres/view_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"view_routine_usage","FullyQualifiedName":"localhost/postgres/view_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"view_routine_usage","FullyQualifiedName":"localhost/postgres/view_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"view_routine_usage","FullyQualifiedName":"localhost/postgres/view_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"view_routine_usage","FullyQualifiedName":"localhost/postgres/view_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"view_routine_usage","FullyQualifiedName":"localhost/postgres/view_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"view_routine_usage","FullyQualifiedName":"localhost/postgres/view_routine_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"view_table_usage","FullyQualifiedName":"localhost/postgres/view_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"view_table_usage","FullyQualifiedName":"localhost/postgres/view_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"view_table_usage","FullyQualifiedName":"localhost/postgres/view_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"view_table_usage","FullyQualifiedName":"localhost/postgres/view_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"view_table_usage","FullyQualifiedName":"localhost/postgres/view_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"view_table_usage","FullyQualifiedName":"localhost/postgres/view_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"view_table_usage","FullyQualifiedName":"localhost/postgres/view_table_usage","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}},{"Resource":{"Name":"views","FullyQualifiedName":"localhost/postgres/views","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"delete","Parent":null}},{"Resource":{"Name":"views","FullyQualifiedName":"localhost/postgres/views","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"insert","Parent":null}},{"Resource":{"Name":"views","FullyQualifiedName":"localhost/postgres/views","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"references","Parent":null}},{"Resource":{"Name":"views","FullyQualifiedName":"localhost/postgres/views","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"select","Parent":null}},{"Resource":{"Name":"views","FullyQualifiedName":"localhost/postgres/views","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"trigger","Parent":null}},{"Resource":{"Name":"views","FullyQualifiedName":"localhost/postgres/views","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"truncate","Parent":null}},{"Resource":{"Name":"views","FullyQualifiedName":"localhost/postgres/views","Type":"table","Metadata":{"rows":"Unknown","size":"0 bytes"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"database","Metadata":{"owner":"postgres"},"Parent":{"Name":"postgres","FullyQualifiedName":"localhost/postgres","Type":"user","Metadata":{"role":"postgres"},"Parent":null}}},"Permission":{"Value":"update","Parent":null}}],"UnboundedResources":null,"Metadata":null}
================================================
FILE: pkg/analyzer/analyzers/postgres/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package postgres
import "errors"
type Permission int
const (
Invalid Permission = iota
BypassRls Permission = iota
Connect Permission = iota
Create Permission = iota
CreateDb Permission = iota
CreateRole Permission = iota
Delete Permission = iota
InheritanceOfPrivs Permission = iota
Insert Permission = iota
Login Permission = iota
References Permission = iota
Replication Permission = iota
Select Permission = iota
Superuser Permission = iota
Temp Permission = iota
Trigger Permission = iota
Truncate Permission = iota
Update Permission = iota
)
var (
PermissionStrings = map[Permission]string{
BypassRls: "bypass_rls",
Connect: "connect",
Create: "create",
CreateDb: "create_db",
CreateRole: "create_role",
Delete: "delete",
InheritanceOfPrivs: "inheritance_of_privs",
Insert: "insert",
Login: "login",
References: "references",
Replication: "replication",
Select: "select",
Superuser: "superuser",
Temp: "temp",
Trigger: "trigger",
Truncate: "truncate",
Update: "update",
}
StringToPermission = map[string]Permission{
"bypass_rls": BypassRls,
"connect": Connect,
"create": Create,
"create_db": CreateDb,
"create_role": CreateRole,
"delete": Delete,
"inheritance_of_privs": InheritanceOfPrivs,
"insert": Insert,
"login": Login,
"references": References,
"replication": Replication,
"select": Select,
"superuser": Superuser,
"temp": Temp,
"trigger": Trigger,
"truncate": Truncate,
"update": Update,
}
PermissionIDs = map[Permission]int{
BypassRls: 1,
Connect: 2,
Create: 3,
CreateDb: 4,
CreateRole: 5,
Delete: 6,
InheritanceOfPrivs: 7,
Insert: 8,
Login: 9,
References: 10,
Replication: 11,
Select: 12,
Superuser: 13,
Temp: 14,
Trigger: 15,
Truncate: 16,
Update: 17,
}
IdToPermission = map[int]Permission{
1: BypassRls,
2: Connect,
3: Create,
4: CreateDb,
5: CreateRole,
6: Delete,
7: InheritanceOfPrivs,
8: Insert,
9: Login,
10: References,
11: Replication,
12: Select,
13: Superuser,
14: Temp,
15: Trigger,
16: Truncate,
17: Update,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/postgres/permissions.yaml
================================================
permissions:
- bypass_rls
- connect
- create
- create_db
- create_role
- delete
- inheritance_of_privs
- insert
- login
- references
- replication
- select
- superuser
- temp
- trigger
- truncate
- update
================================================
FILE: pkg/analyzer/analyzers/postgres/postgres.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go postgres
package postgres
import (
"database/sql"
"errors"
"fmt"
"os"
"regexp"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/lib/pq"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypePostgres }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
uri, ok := credInfo["connection_string"]
if !ok {
return nil, errors.New("connection string not found in credInfo")
}
info, err := AnalyzePermissions(a.Cfg, uri)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypePostgres,
Metadata: nil,
Bindings: []analyzers.Binding{},
}
// set user related bindings in result
userResource, userBindings := bakeUserBindings(info)
result.Bindings = append(result.Bindings, userBindings...)
// add user's database privileges to bindings
dbNameToResourceMap, dbBindings := bakeDatabaseBindings(userResource, info)
result.Bindings = append(result.Bindings, dbBindings...)
// add user's table privileges to bindings
tableBindings := bakeTableBindings(dbNameToResourceMap, info)
result.Bindings = append(result.Bindings, tableBindings...)
return &result
}
func bakeUserBindings(info *SecretInfo) (analyzers.Resource, []analyzers.Binding) {
userResource := analyzers.Resource{
Name: info.User,
FullyQualifiedName: info.Host + "/" + info.User,
Type: "user",
Metadata: map[string]any{
"role": info.Role,
},
}
var bindings []analyzers.Binding
for rolePriv, exists := range info.RolePrivs {
if exists {
bindings = append(bindings, analyzers.Binding{
Resource: userResource,
Permission: analyzers.Permission{
Value: rolePriv,
},
})
}
}
return userResource, bindings
}
func bakeDatabaseBindings(userResource analyzers.Resource, info *SecretInfo) (map[string]*analyzers.Resource, []analyzers.Binding) {
dbNameToResourceMap := map[string]*analyzers.Resource{}
dbBindings := []analyzers.Binding{}
for _, db := range info.DBs {
dbResource := analyzers.Resource{
Name: db.DatabaseName,
FullyQualifiedName: info.Host + "/" + db.DatabaseName,
Type: "database",
Metadata: map[string]any{
"owner": db.Owner,
},
Parent: &userResource,
}
// populate map to reference later for tables
dbNameToResourceMap[db.DatabaseName] = &dbResource
dbPriviliges := map[string]bool{
"connect": db.Connect,
"create": db.Create,
"temp": db.CreateTemp,
}
for priv, exists := range dbPriviliges {
if exists {
dbBindings = append(dbBindings, analyzers.Binding{
Resource: dbResource,
Permission: analyzers.Permission{
Value: priv,
},
})
}
}
}
return dbNameToResourceMap, dbBindings
}
func bakeTableBindings(dbNameToResourceMap map[string]*analyzers.Resource, info *SecretInfo) []analyzers.Binding {
var tableBindings []analyzers.Binding
for dbName, tableMap := range info.TablePrivs {
dbResource, ok := dbNameToResourceMap[dbName]
if !ok {
continue
}
for tableName, tableData := range tableMap {
tableResource := analyzers.Resource{
Name: tableName,
FullyQualifiedName: info.Host + "/" + dbResource.Name + "/" + tableName,
Type: "table",
Metadata: map[string]any{
"size": tableData.Size,
"rows": tableData.Rows,
},
Parent: dbResource,
}
tablePrivsMap := map[string]bool{
"select": tableData.Privs.Select,
"insert": tableData.Privs.Insert,
"update": tableData.Privs.Update,
"delete": tableData.Privs.Delete,
"truncate": tableData.Privs.Truncate,
"references": tableData.Privs.References,
"trigger": tableData.Privs.Trigger,
}
for priv, exists := range tablePrivsMap {
if exists {
tableBindings = append(tableBindings, analyzers.Binding{
Resource: tableResource,
Permission: analyzers.Permission{
Value: priv,
},
})
}
}
}
}
return tableBindings
}
type DBPrivs struct {
Connect bool
Create bool
CreateTemp bool
}
type DB struct {
DatabaseName string
Owner string
DBPrivs
}
type TablePrivs struct {
Select bool
Insert bool
Update bool
Delete bool
Truncate bool
References bool
Trigger bool
}
type TableData struct {
Size string
Rows string
Privs TablePrivs
}
const (
pg_connect_timeout = "connect_timeout"
pg_dbname = "dbname"
pg_host = "host"
pg_password = "password"
pg_port = "port"
pg_requiressl = "requiressl"
pg_sslmode = "sslmode"
pg_sslmode_allow = "allow"
pg_sslmode_disable = "disable"
pg_sslmode_prefer = "prefer"
pg_sslmode_require = "require"
pg_user = "user"
)
var connStrPartPattern = regexp.MustCompile(`([[:alpha:]]+)='(.+?)' ?`)
type SecretInfo struct {
Host string
User string
Role string
RolePrivs map[string]bool
DBs []DB
TablePrivs map[string]map[string]*TableData
}
func AnalyzeAndPrintPermissions(cfg *config.Config, connectionStr string) {
// ToDo: Add in logging
if cfg.LoggingEnabled {
color.Red("[x] Logging is not supported for this analyzer.")
return
}
info, err := AnalyzePermissions(cfg, connectionStr)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
color.Yellow("[!] Successfully connected to Postgres database.")
printUserRoleAndPriv(info.Role, info.RolePrivs)
// Print db privs
if len(info.DBs) > 0 {
fmt.Print("\n\n")
color.Green("[i] User has the following database privileges:")
printDBPrivs(info.DBs, info.User)
}
// Print table privs
if len(info.TablePrivs) > 0 {
fmt.Print("\n\n")
color.Green("[i] User has the following table privileges:")
printTablePrivs(info.TablePrivs)
}
}
func AnalyzePermissions(cfg *config.Config, connectionStr string) (*SecretInfo, error) {
connStr, err := pq.ParseURL(string(connectionStr))
if err != nil {
return nil, fmt.Errorf("failed to parse Postgres connection string: %w", err)
}
parts := connStrPartPattern.FindAllStringSubmatch(connStr, -1)
params := make(map[string]string, len(parts))
for _, part := range parts {
params[part[1]] = part[2]
}
db, err := createConnection(params, "")
if err != nil {
return nil, fmt.Errorf("failed to connect to Postgres database: %w", err)
}
defer db.Close()
role, privs, err := getUserPrivs(db)
if err != nil {
return nil, fmt.Errorf("failed to retrieve user privileges: %w", err)
}
currentUser, dbs, err := getDBPrivs(db)
if err != nil {
return nil, fmt.Errorf("failed to retrieve database privileges: %w", err)
}
tablePrivs, err := getTablePrivs(params, buildSliceDBNames(dbs))
if err != nil {
return nil, fmt.Errorf("failed to retrieve table privileges: %w", err)
}
return &SecretInfo{
Host: params[pg_host],
User: currentUser,
Role: role,
RolePrivs: privs,
DBs: dbs,
TablePrivs: tablePrivs,
}, nil
}
func isErrorDatabaseNotFound(err error, dbName string, user string) bool {
options := []string{dbName, user, "postgres"}
for _, option := range options {
if strings.Contains(err.Error(), fmt.Sprintf("database \"%s\" does not exist", option)) {
return true
}
}
return false
}
func createConnection(params map[string]string, database string) (*sql.DB, error) {
if sslmode := params[pg_sslmode]; sslmode == pg_sslmode_allow || sslmode == pg_sslmode_prefer {
// pq doesn't support 'allow' or 'prefer'. If we find either of them, we'll just ignore it. This will trigger
// the same logic that is run if no sslmode is set at all (which mimics 'prefer', which is the default).
delete(params, pg_sslmode)
}
var connStr string
for key, value := range params {
if database != "" && key == "dbname" {
connStr += fmt.Sprintf("%s='%s'", key, database)
} else {
connStr += fmt.Sprintf("%s='%s'", key, value)
}
}
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, err
}
err = db.Ping()
switch {
case err == nil:
return db, nil
case strings.Contains(err.Error(), "password authentication failed"):
return nil, errors.New("password authentication failed")
case errors.Is(err, pq.ErrSSLNotSupported) && params[pg_sslmode] == "":
// If the sslmode is unset, then either it was unset in the candidate secret, or we've intentionally unset it
// because it was specified as 'allow' or 'prefer', neither of which pq supports. In all of these cases, non-SSL
// connections are acceptable, so now we try a connection without SSL.
params[pg_sslmode] = pg_sslmode_disable
defer delete(params, pg_sslmode) // We want to return with the original params map intact (for ExtraData)
return createConnection(params, database)
case isErrorDatabaseNotFound(err, params[pg_dbname], params[pg_user]):
color.Green("[!] Successfully connected to Postgres database.")
return nil, err
default:
return nil, err
}
}
func getUserPrivs(db *sql.DB) (string, map[string]bool, error) {
// Prepare the SQL statement
query := `SELECT rolname AS role_name,
rolsuper AS is_superuser,
rolinherit AS can_inherit,
rolcreaterole AS can_create_role,
rolcreatedb AS can_create_db,
rolcanlogin AS can_login,
rolreplication AS is_replication_role,
rolbypassrls AS bypasses_rls
FROM pg_roles WHERE rolname = current_user;`
// Execute the SQL query
rows, err := db.Query(query)
if err != nil {
return "", nil, err
}
defer rows.Close()
var roleName string
var isSuperuser, canInherit, canCreateRole, canCreateDB, canLogin, isReplicationRole, bypassesRLS bool
// Iterate over the rows
for rows.Next() {
if err := rows.Scan(&roleName, &isSuperuser, &canInherit, &canCreateRole, &canCreateDB, &canLogin, &isReplicationRole, &bypassesRLS); err != nil {
return "", nil, err
}
}
// Check for errors during iteration
if err := rows.Err(); err != nil {
return "", nil, err
}
// Map roles to privileges
var mapRoles map[string]bool = map[string]bool{
"Superuser": isSuperuser,
"Inheritance of Privs": canInherit,
"Create Role": canCreateRole,
"Create DB": canCreateDB,
"Login": canLogin,
"Replication": isReplicationRole,
"Bypass RLS": bypassesRLS,
}
return roleName, mapRoles, nil
}
func getDBPrivs(db *sql.DB) (string, []DB, error) {
query := `
SELECT
d.datname AS database_name,
u.usename AS owner,
current_user AS current_user,
has_database_privilege(current_user, d.datname, 'CONNECT') AS can_connect,
has_database_privilege(current_user, d.datname, 'CREATE') AS can_create,
has_database_privilege(current_user, d.datname, 'TEMP') AS can_create_temporary_tables
FROM
pg_database d
JOIN
pg_user u ON d.datdba = u.usesysid
WHERE
NOT d.datistemplate
ORDER BY
d.datname;
`
// Originally had WHERE NOT d.datistemplate AND d.datallowconn
// Execute the query
rows, err := db.Query(query)
if err != nil {
return "", nil, err
}
defer rows.Close()
dbs := make([]DB, 0)
var currentUser string
// Iterate through the result set
for rows.Next() {
var dbName, owner string
var canConnect, canCreate, canCreateTemp bool
err := rows.Scan(&dbName, &owner, ¤tUser, &canConnect, &canCreate, &canCreateTemp)
if err != nil {
return "", nil, err
}
db := DB{
DatabaseName: dbName,
Owner: owner,
DBPrivs: DBPrivs{
Connect: canConnect,
Create: canCreate,
CreateTemp: canCreateTemp,
},
}
dbs = append(dbs, db)
}
if err = rows.Err(); err != nil {
return "", nil, err
}
return currentUser, dbs, nil
}
func printDBPrivs(dbs []DB, current_user string) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Database", "Owner", "Access Privileges"})
for _, db := range dbs {
privs := buildDBPrivsStr(db)
writer := getDBWriter(db, current_user)
t.AppendRow([]interface{}{writer(db.DatabaseName), writer(db.Owner), writer(privs)})
}
t.Render()
}
func buildDBPrivsStr(db DB) string {
privs := ""
if db.Connect {
privs += "CONNECT"
}
if db.Create {
privs += ", CREATE"
}
if db.CreateTemp {
privs += ", TEMP"
}
privs = strings.TrimPrefix(privs, ", ")
return privs
}
func getDBWriter(db DB, current_user string) func(a ...interface{}) string {
if db.Owner == current_user {
return analyzers.GreenWriter
} else if db.Connect && db.Create && db.CreateTemp {
return analyzers.GreenWriter
} else if db.Connect || db.Create || db.CreateTemp {
return analyzers.YellowWriter
} else {
return analyzers.DefaultWriter
}
}
func buildSliceDBNames(dbs []DB) []string {
var dbNames []string
for _, db := range dbs {
if db.DBPrivs.Connect {
dbNames = append(dbNames, db.DatabaseName)
}
}
return dbNames
}
func getTablePrivs(params map[string]string, databases []string) (map[string]map[string]*TableData, error) {
tablePrivileges := make(map[string]map[string]*TableData, 0)
for _, dbase := range databases {
// Connect to db
db, err := createConnection(params, dbase)
if err != nil {
// color.Red("[x] Failed to connect to Postgres database: %s", dbase)
continue
}
defer db.Close()
// Get table privs
query := `
SELECT
rtg.table_catalog,
rtg.table_name,
rtg.privilege_type,
pg_size_pretty(pg_total_relation_size(pc.oid)) AS table_size,
pc.reltuples AS estimate
FROM
information_schema.role_table_grants rtg
JOIN
pg_catalog.pg_class pc ON rtg.table_name = pc.relname
WHERE
rtg.grantee = current_user;
`
// Execute the query
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
// Iterate through the result set
for rows.Next() {
var database, table, priv, size, row_count string
err := rows.Scan(&database, &table, &priv, &size, &row_count)
if err != nil {
return nil, err
}
if _, ok := tablePrivileges[database]; !ok {
tablePrivileges[database] = map[string]*TableData{
table: {},
}
}
if _, ok := tablePrivileges[database][table]; !ok {
tablePrivileges[database][table] = &TableData{}
}
switch priv {
case "SELECT":
tablePrivileges[database][table].Privs.Select = true
case "INSERT":
tablePrivileges[database][table].Privs.Insert = true
case "UPDATE":
tablePrivileges[database][table].Privs.Update = true
case "DELETE":
tablePrivileges[database][table].Privs.Delete = true
case "TRUNCATE":
tablePrivileges[database][table].Privs.Truncate = true
case "REFERENCES":
tablePrivileges[database][table].Privs.References = true
case "TRIGGER":
tablePrivileges[database][table].Privs.Trigger = true
}
tablePrivileges[database][table].Size = size
if row_count != "-1" {
tablePrivileges[database][table].Rows = row_count
} else {
tablePrivileges[database][table].Rows = "Unknown"
}
}
if err = rows.Err(); err != nil {
return nil, err
}
db.Close()
}
return tablePrivileges, nil
}
func printTablePrivs(tables map[string]map[string]*TableData) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Database", "Table", "Access Privileges", "Est. Size", "Est. Rows"})
var writer func(a ...interface{}) string
for db, table := range tables {
for table_name, tableData := range table {
privs := tableData.Privs
privsStr := buildTablePrivsStr(privs)
if privsStr == "" {
writer = color.New().SprintFunc()
} else {
writer = color.New(color.FgGreen).SprintFunc()
}
t.AppendRow([]interface{}{writer(db), writer(table_name), writer(privsStr), writer("< " + tableData.Size), writer(tableData.Rows)})
}
}
t.Render()
}
func printUserRoleAndPriv(role string, privs map[string]bool) {
color.Yellow("[i] User: %s", role)
color.Yellow("[i] Privileges: ")
for role, priv := range privs {
if role == "Superuser" && priv {
color.Green(" - %s", role)
} else if priv {
color.Yellow(" - %s", role)
}
}
}
func buildTablePrivsStr(privs TablePrivs) string {
var privsStr string
if privs.Select {
privsStr += "SELECT"
}
if privs.Insert {
privsStr += ", INSERT"
}
if privs.Update {
privsStr += ", UPDATE"
}
if privs.Delete {
privsStr += ", DELETE"
}
if privs.Truncate {
privsStr += ", TRUNCATE"
}
if privs.References {
privsStr += ", REFERENCES"
}
if privs.Trigger {
privsStr += ", TRIGGER"
}
privsStr = strings.TrimPrefix(privsStr, ", ")
return privsStr
}
================================================
FILE: pkg/analyzer/analyzers/postgres/postgres_test.go
================================================
package postgres
import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"fmt"
"os/exec"
"sort"
"strings"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
const (
postgresUser = "postgres"
postgresPass = "23201da=b56ca236f3dc6736c0f9afad"
postgresHost = "localhost"
postgresPort = "5434" // Do not use 5433, as local dev environments can use it for other things
defaultPort = "5432"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
if err := startPostgres(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
t.Fatalf("could not start local postgres: %v w/stderr:\n%s", err, string(exitErr.Stderr))
} else {
t.Fatalf("could not start local postgres: %v", err)
}
}
defer stopPostgres()
tests := []struct {
name string
connectionString string
want []byte // JSON string
wantErr bool
}{
{
name: "valid Postgres connection",
connectionString: fmt.Sprintf(`postgresql://%s:%s@%s:%s/postgres`, postgresUser, postgresPass, postgresHost, postgresPort),
want: expectedOutput,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(context.Background(), map[string]string{"connection_string": tt.connectionString})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal(tt.want, &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
var postgresDockerHash string
func dockerLogLine(hash string, needle string) chan struct{} {
ch := make(chan struct{}, 1)
go func() {
for {
out, err := exec.Command("docker", "logs", hash).CombinedOutput()
if err != nil {
panic(err)
}
if strings.Contains(string(out), needle) {
ch <- struct{}{}
return
}
time.Sleep(1 * time.Second)
}
}()
return ch
}
func startPostgres() error {
cmd := exec.Command(
"docker", "run", "--rm", "-p", postgresPort+":"+defaultPort,
"-e", "POSTGRES_PASSWORD="+postgresPass,
"-e", "POSTGRES_USER="+postgresUser,
"-d", "postgres",
)
fmt.Println(cmd.String())
out, err := cmd.Output()
if err != nil {
return err
}
postgresDockerHash = string(bytes.TrimSpace(out))
select {
case <-dockerLogLine(postgresDockerHash, "PostgreSQL init process complete; ready for start up."):
return nil
case <-time.After(30 * time.Second):
stopPostgres()
return errors.New("timeout waiting for postgres database to be ready")
}
}
func stopPostgres() {
err := exec.Command("docker", "kill", postgresDockerHash).Run()
if err != nil {
fmt.Println("could not stop postgres container:", err)
}
}
================================================
FILE: pkg/analyzer/analyzers/posthog/expected_output.json
================================================
{"AnalyzerType":39,"Bindings":[{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"action:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"activity_log:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"annotation:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"dashboard:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"event_definition:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"event_definition:write","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"export:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"group:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"group:write","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"insight:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"person:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"person:write","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"query:read","Parent":null}},{"Resource":{"Name":"Truffle Security","FullyQualifiedName":"019666bb-9f8e-0000-8bc2-4ea34ec57752","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"user:read","Parent":null}},{"Resource":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null},"Permission":{"Value":"organization:read","Parent":null}},{"Resource":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null},"Permission":{"Value":"project:read","Parent":null}}],"UnboundedResources":null,"Metadata":null}
================================================
FILE: pkg/analyzer/analyzers/posthog/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package posthog
import "errors"
type Permission int
const (
Invalid Permission = iota
ActionRead Permission = iota
ActionWrite Permission = iota
ActivityLogRead Permission = iota
ActivityLogWrite Permission = iota
AnnotationRead Permission = iota
AnnotationWrite Permission = iota
BatchExportRead Permission = iota
BatchExportWrite Permission = iota
CohortRead Permission = iota
CohortWrite Permission = iota
DashboardRead Permission = iota
DashboardWrite Permission = iota
DashboardTemplateRead Permission = iota
DashboardTemplateWrite Permission = iota
EarlyAccessFeatureRead Permission = iota
EarlyAccessFeatureWrite Permission = iota
EventDefinitionRead Permission = iota
EventDefinitionWrite Permission = iota
ErrorTrackingRead Permission = iota
ErrorTrackingWrite Permission = iota
ExperimentRead Permission = iota
ExperimentWrite Permission = iota
ExportRead Permission = iota
ExportWrite Permission = iota
FeatureFlagRead Permission = iota
FeatureFlagWrite Permission = iota
GroupRead Permission = iota
GroupWrite Permission = iota
HogFunctionRead Permission = iota
HogFunctionWrite Permission = iota
InsightRead Permission = iota
InsightWrite Permission = iota
NotebookRead Permission = iota
NotebookWrite Permission = iota
OrganizationRead Permission = iota
OrganizationWrite Permission = iota
OrganizationMemberRead Permission = iota
OrganizationMemberWrite Permission = iota
PersonRead Permission = iota
PersonWrite Permission = iota
PluginRead Permission = iota
PluginWrite Permission = iota
ProjectRead Permission = iota
ProjectWrite Permission = iota
PropertyDefinitionRead Permission = iota
PropertyDefinitionWrite Permission = iota
QueryRead Permission = iota
SessionRecordingRead Permission = iota
SessionRecordingWrite Permission = iota
SessionRecordingPlaylistRead Permission = iota
SessionRecordingPlaylistWrite Permission = iota
SharingConfigurationRead Permission = iota
SharingConfigurationWrite Permission = iota
SubscriptionRead Permission = iota
SubscriptionWrite Permission = iota
SurveyRead Permission = iota
SurveyWrite Permission = iota
UserRead Permission = iota
WebhookRead Permission = iota
WebhookWrite Permission = iota
)
var (
PermissionStrings = map[Permission]string{
ActionRead: "action:read",
ActionWrite: "action:write",
ActivityLogRead: "activity_log:read",
ActivityLogWrite: "activity_log:write",
AnnotationRead: "annotation:read",
AnnotationWrite: "annotation:write",
BatchExportRead: "batch_export:read",
BatchExportWrite: "batch_export:write",
CohortRead: "cohort:read",
CohortWrite: "cohort:write",
DashboardRead: "dashboard:read",
DashboardWrite: "dashboard:write",
DashboardTemplateRead: "dashboard_template:read",
DashboardTemplateWrite: "dashboard_template:write",
EarlyAccessFeatureRead: "early_access_feature:read",
EarlyAccessFeatureWrite: "early_access_feature:write",
EventDefinitionRead: "event_definition:read",
EventDefinitionWrite: "event_definition:write",
ErrorTrackingRead: "error_tracking:read",
ErrorTrackingWrite: "error_tracking:write",
ExperimentRead: "experiment:read",
ExperimentWrite: "experiment:write",
ExportRead: "export:read",
ExportWrite: "export:write",
FeatureFlagRead: "feature_flag:read",
FeatureFlagWrite: "feature_flag:write",
GroupRead: "group:read",
GroupWrite: "group:write",
HogFunctionRead: "hog_function:read",
HogFunctionWrite: "hog_function:write",
InsightRead: "insight:read",
InsightWrite: "insight:write",
NotebookRead: "notebook:read",
NotebookWrite: "notebook:write",
OrganizationRead: "organization:read",
OrganizationWrite: "organization:write",
OrganizationMemberRead: "organization_member:read",
OrganizationMemberWrite: "organization_member:write",
PersonRead: "person:read",
PersonWrite: "person:write",
PluginRead: "plugin:read",
PluginWrite: "plugin:write",
ProjectRead: "project:read",
ProjectWrite: "project:write",
PropertyDefinitionRead: "property_definition:read",
PropertyDefinitionWrite: "property_definition:write",
QueryRead: "query:read",
SessionRecordingRead: "session_recording:read",
SessionRecordingWrite: "session_recording:write",
SessionRecordingPlaylistRead: "session_recording_playlist:read",
SessionRecordingPlaylistWrite: "session_recording_playlist:write",
SharingConfigurationRead: "sharing_configuration:read",
SharingConfigurationWrite: "sharing_configuration:write",
SubscriptionRead: "subscription:read",
SubscriptionWrite: "subscription:write",
SurveyRead: "survey:read",
SurveyWrite: "survey:write",
UserRead: "user:read",
WebhookRead: "webhook:read",
WebhookWrite: "webhook:write",
}
StringToPermission = map[string]Permission{
"action:read": ActionRead,
"action:write": ActionWrite,
"activity_log:read": ActivityLogRead,
"activity_log:write": ActivityLogWrite,
"annotation:read": AnnotationRead,
"annotation:write": AnnotationWrite,
"batch_export:read": BatchExportRead,
"batch_export:write": BatchExportWrite,
"cohort:read": CohortRead,
"cohort:write": CohortWrite,
"dashboard:read": DashboardRead,
"dashboard:write": DashboardWrite,
"dashboard_template:read": DashboardTemplateRead,
"dashboard_template:write": DashboardTemplateWrite,
"early_access_feature:read": EarlyAccessFeatureRead,
"early_access_feature:write": EarlyAccessFeatureWrite,
"event_definition:read": EventDefinitionRead,
"event_definition:write": EventDefinitionWrite,
"error_tracking:read": ErrorTrackingRead,
"error_tracking:write": ErrorTrackingWrite,
"experiment:read": ExperimentRead,
"experiment:write": ExperimentWrite,
"export:read": ExportRead,
"export:write": ExportWrite,
"feature_flag:read": FeatureFlagRead,
"feature_flag:write": FeatureFlagWrite,
"group:read": GroupRead,
"group:write": GroupWrite,
"hog_function:read": HogFunctionRead,
"hog_function:write": HogFunctionWrite,
"insight:read": InsightRead,
"insight:write": InsightWrite,
"notebook:read": NotebookRead,
"notebook:write": NotebookWrite,
"organization:read": OrganizationRead,
"organization:write": OrganizationWrite,
"organization_member:read": OrganizationMemberRead,
"organization_member:write": OrganizationMemberWrite,
"person:read": PersonRead,
"person:write": PersonWrite,
"plugin:read": PluginRead,
"plugin:write": PluginWrite,
"project:read": ProjectRead,
"project:write": ProjectWrite,
"property_definition:read": PropertyDefinitionRead,
"property_definition:write": PropertyDefinitionWrite,
"query:read": QueryRead,
"session_recording:read": SessionRecordingRead,
"session_recording:write": SessionRecordingWrite,
"session_recording_playlist:read": SessionRecordingPlaylistRead,
"session_recording_playlist:write": SessionRecordingPlaylistWrite,
"sharing_configuration:read": SharingConfigurationRead,
"sharing_configuration:write": SharingConfigurationWrite,
"subscription:read": SubscriptionRead,
"subscription:write": SubscriptionWrite,
"survey:read": SurveyRead,
"survey:write": SurveyWrite,
"user:read": UserRead,
"webhook:read": WebhookRead,
"webhook:write": WebhookWrite,
}
PermissionIDs = map[Permission]int{
ActionRead: 1,
ActionWrite: 2,
ActivityLogRead: 3,
ActivityLogWrite: 4,
AnnotationRead: 5,
AnnotationWrite: 6,
BatchExportRead: 7,
BatchExportWrite: 8,
CohortRead: 9,
CohortWrite: 10,
DashboardRead: 11,
DashboardWrite: 12,
DashboardTemplateRead: 13,
DashboardTemplateWrite: 14,
EarlyAccessFeatureRead: 15,
EarlyAccessFeatureWrite: 16,
EventDefinitionRead: 17,
EventDefinitionWrite: 18,
ErrorTrackingRead: 19,
ErrorTrackingWrite: 20,
ExperimentRead: 21,
ExperimentWrite: 22,
ExportRead: 23,
ExportWrite: 24,
FeatureFlagRead: 25,
FeatureFlagWrite: 26,
GroupRead: 27,
GroupWrite: 28,
HogFunctionRead: 29,
HogFunctionWrite: 30,
InsightRead: 31,
InsightWrite: 32,
NotebookRead: 33,
NotebookWrite: 34,
OrganizationRead: 35,
OrganizationWrite: 36,
OrganizationMemberRead: 37,
OrganizationMemberWrite: 38,
PersonRead: 39,
PersonWrite: 40,
PluginRead: 41,
PluginWrite: 42,
ProjectRead: 43,
ProjectWrite: 44,
PropertyDefinitionRead: 45,
PropertyDefinitionWrite: 46,
QueryRead: 47,
SessionRecordingRead: 48,
SessionRecordingWrite: 49,
SessionRecordingPlaylistRead: 50,
SessionRecordingPlaylistWrite: 51,
SharingConfigurationRead: 52,
SharingConfigurationWrite: 53,
SubscriptionRead: 54,
SubscriptionWrite: 55,
SurveyRead: 56,
SurveyWrite: 57,
UserRead: 58,
WebhookRead: 59,
WebhookWrite: 60,
}
IdToPermission = map[int]Permission{
1: ActionRead,
2: ActionWrite,
3: ActivityLogRead,
4: ActivityLogWrite,
5: AnnotationRead,
6: AnnotationWrite,
7: BatchExportRead,
8: BatchExportWrite,
9: CohortRead,
10: CohortWrite,
11: DashboardRead,
12: DashboardWrite,
13: DashboardTemplateRead,
14: DashboardTemplateWrite,
15: EarlyAccessFeatureRead,
16: EarlyAccessFeatureWrite,
17: EventDefinitionRead,
18: EventDefinitionWrite,
19: ErrorTrackingRead,
20: ErrorTrackingWrite,
21: ExperimentRead,
22: ExperimentWrite,
23: ExportRead,
24: ExportWrite,
25: FeatureFlagRead,
26: FeatureFlagWrite,
27: GroupRead,
28: GroupWrite,
29: HogFunctionRead,
30: HogFunctionWrite,
31: InsightRead,
32: InsightWrite,
33: NotebookRead,
34: NotebookWrite,
35: OrganizationRead,
36: OrganizationWrite,
37: OrganizationMemberRead,
38: OrganizationMemberWrite,
39: PersonRead,
40: PersonWrite,
41: PluginRead,
42: PluginWrite,
43: ProjectRead,
44: ProjectWrite,
45: PropertyDefinitionRead,
46: PropertyDefinitionWrite,
47: QueryRead,
48: SessionRecordingRead,
49: SessionRecordingWrite,
50: SessionRecordingPlaylistRead,
51: SessionRecordingPlaylistWrite,
52: SharingConfigurationRead,
53: SharingConfigurationWrite,
54: SubscriptionRead,
55: SubscriptionWrite,
56: SurveyRead,
57: SurveyWrite,
58: UserRead,
59: WebhookRead,
60: WebhookWrite,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/posthog/permissions.yaml
================================================
permissions:
- action:read
- action:write
- activity_log:read
- activity_log:write
- annotation:read
- annotation:write
- batch_export:read
- batch_export:write
- cohort:read
- cohort:write
- dashboard:read
- dashboard:write
- dashboard_template:read
- dashboard_template:write
- early_access_feature:read
- early_access_feature:write
- event_definition:read
- event_definition:write
- error_tracking:read
- error_tracking:write
- experiment:read
- experiment:write
- export:read
- export:write
- feature_flag:read
- feature_flag:write
- group:read
- group:write
- hog_function:read
- hog_function:write
- insight:read
- insight:write
- notebook:read
- notebook:write
- organization:read
- organization:write
- organization_member:read
- organization_member:write
- person:read
- person:write
- plugin:read
- plugin:write
- project:read
- project:write
- property_definition:read
- property_definition:write
- query:read
- session_recording:read
- session_recording:write
- session_recording_playlist:read
- session_recording_playlist:write
- sharing_configuration:read
- sharing_configuration:write
- subscription:read
- subscription:write
- survey:read
- survey:write
- user:read
- webhook:read
- webhook:write
================================================
FILE: pkg/analyzer/analyzers/posthog/posthog.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go posthog
package posthog
import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
const (
USDomain = "https://us.posthog.com"
EUDomain = "https://eu.posthog.com"
)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypePosthog }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, errors.New("missing key in credInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypePosthog,
Metadata: nil,
Bindings: make([]analyzers.Binding, 0),
}
if info.orgPermissions == nil {
// no permissions to check
return &result
}
if info.user != nil {
// for user resource
userResource := analyzers.Resource{
Name: info.user.FirstName + " " + info.user.LastName,
FullyQualifiedName: info.user.UUID,
Type: "user",
}
analyzerPermission := analyzers.Permission{
Value: PermissionStrings[UserRead],
}
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: userResource,
Permission: analyzerPermission,
})
}
// for organization permissions, we need to bind the permissions to the organization resource
organizationResource := analyzers.Resource{
Name: info.organization.Name,
FullyQualifiedName: info.organization.ID,
Type: "organization",
}
for _, permission := range info.orgPermissions {
if value, ok := PermissionStrings[permission]; ok {
analyzerPermission := analyzers.Permission{
Value: value,
}
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: organizationResource,
Permission: analyzerPermission,
})
}
}
// for project permissions, we need to bind the permissions to the project resource and organization as the parent resource
for _, projectPermission := range info.projectPermissions {
projectResource := analyzers.Resource{
Name: projectPermission.Project.Name,
FullyQualifiedName: strconv.FormatInt(projectPermission.Project.ID, 10),
Type: "project",
Parent: &organizationResource,
}
for _, permission := range projectPermission.Permissions {
permissionStr, _ := permission.ToString()
analyzerPermission := analyzers.Permission{
Value: permissionStr,
}
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: projectResource,
Permission: analyzerPermission,
})
}
}
return &result
}
//go:embed scopes.json
var scopesConfigBytes []byte
type HttpStatusTest struct {
Endpoint string `json:"endpoint"`
Method string `json:"method"`
Payload interface{} `json:"payload"`
ValidStatuses []int `json:"valid_status_code"`
InvalidStatuses []int `json:"invalid_status_code"`
}
func StatusContains(status int, vals []int) bool {
for _, v := range vals {
if status == v {
return true
}
}
return false
}
func (h *HttpStatusTest) RunTest(cfg *config.Config, client *http.Client, domain string, headers map[string]string, args ...any) (bool, error) {
// If body data, marshal to JSON
var data io.Reader
if h.Payload != nil {
jsonData, err := json.Marshal(h.Payload)
if err != nil {
return false, err
}
data = bytes.NewBuffer(jsonData)
}
req, err := http.NewRequest(h.Method, fmt.Sprintf(domain+h.Endpoint, args...), data)
if err != nil {
return false, err
}
// Add custom headers if provided
for key, value := range headers {
req.Header.Set(key, value)
}
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// Check response status code
switch {
case StatusContains(resp.StatusCode, h.ValidStatuses):
return true, nil
case StatusContains(resp.StatusCode, h.InvalidStatuses):
return false, nil
default:
fmt.Println(h.Method, h.Endpoint)
return false, errors.New("error checking response status code")
}
}
type ScopesConfig struct {
GeneralScopes []Scope `json:"general_scopes"`
OrganizationScopes []Scope `json:"organization_scopes"`
ProjectScopes []Scope `json:"project_scopes"`
}
type Scope struct {
Name string `json:"name"`
Test ScopeTest `json:"test"`
}
type ScopeTest struct {
Read *HttpStatusTest `json:"read"`
Write *HttpStatusTest `json:"write"`
}
func readInScopesConfig() (*ScopesConfig, error) {
var scopesConfig ScopesConfig
if err := json.Unmarshal(scopesConfigBytes, &scopesConfig); err != nil {
return nil, err
}
return &scopesConfig, nil
}
func checkPermissions(cfg *config.Config, client *http.Client, domain string, key string, scopes []Scope, args ...any) ([]Permission, error) {
permissions := make([]Permission, 0)
headers := map[string]string{"Authorization": "Bearer " + key}
for _, scope := range scopes {
var status bool
var err error
if scope.Test.Write != nil {
status, err = scope.Test.Write.RunTest(cfg, client, domain, headers, args...)
if err != nil {
return nil, fmt.Errorf("running test: %w", err)
}
}
if status {
if permission, ok := StringToPermission[scope.Name+":write"]; ok {
permissions = append(permissions, permission)
}
// if write exists, read also exists
if permission, ok := StringToPermission[scope.Name+":read"]; ok {
permissions = append(permissions, permission)
}
} else {
status, err = scope.Test.Read.RunTest(cfg, client, domain, headers, args...)
if err != nil {
return nil, fmt.Errorf("running test: %w", err)
}
if status {
if permission, ok := StringToPermission[scope.Name+":read"]; ok {
permissions = append(permissions, permission)
}
}
}
}
return permissions, nil
}
type ProjectPermissions struct {
Project *Project
Permissions []Permission
}
type SecretInfo struct {
user *User
organization *Organization
orgPermissions []Permission
projectPermissions []ProjectPermissions
// generalPermissions []Permission
unverifiedPermissions map[Permission]struct{}
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error : %s", err.Error())
return
}
color.Green("[!] Valid Posthog API key")
color.Yellow("[i] Expires: Never")
if info.user != nil {
printUser(*info.user)
}
if info.organization == nil {
color.Yellow("\n[i] No permissions were verified for this key because the key does not have one of the necessary permissions (user:read or organization:read) required to verify other permissions.")
}
if info.orgPermissions != nil {
printOrganizationPermissions(*info.organization, info.orgPermissions)
}
if len(info.projectPermissions) > 0 {
printProjectPermissions(info.projectPermissions)
}
printUnverifiedPermissions(info.unverifiedPermissions)
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
var info = &SecretInfo{}
// These are permissions that cannot be verified due to no endpoint available
info.unverifiedPermissions = map[Permission]struct{}{
ErrorTrackingRead: {},
ErrorTrackingWrite: {},
SharingConfigurationRead: {},
SharingConfigurationWrite: {},
WebhookRead: {},
WebhookWrite: {},
}
client := analyzers.NewAnalyzeClient(cfg)
// we need to determine if the key is for US or EU domain
domain, user, err := resolveDomainAndUser(cfg, client, key)
if err != nil {
return nil, fmt.Errorf("Invalid API Key: %w", err)
}
info.user = user
// Most posthog API scopes are bound to projects and organization, so to determine the scopes we need to first get the organization and projects.
// If the key has user:read scope, we will get the user above which contains the organizations and projects.
// If the key does not have user:read scope, we can call the /organizations/@current endpoint to get the
// organization and projects. If the key does not have organization:read scope as well, we cannot determine any scope.
var org *Organization
if user == nil {
org, err = getOrganization(cfg, client, domain, key)
if err != nil {
return nil, err
}
if org == nil {
// can't determine any scopes
for permission := range PermissionStrings {
info.unverifiedPermissions[permission] = struct{}{}
}
return info, nil
}
} else {
org = &user.Organization
}
// set the organization in the info struct
info.organization = org
// read in scopes
scopesConfig, err := readInScopesConfig()
if err != nil {
return nil, err
}
// check organization permissions
organizationPermissions, err := checkOrganizationPermissions(cfg, client, domain, key, scopesConfig, org)
if err != nil {
return nil, err
}
// check general permissions
generalOrganizationPermissions, err := checkGeneralPermissions(cfg, client, domain, key, scopesConfig)
if err != nil {
return nil, err
}
// merge general permissions with organization permissions
info.orgPermissions = organizationPermissions
info.orgPermissions = append(info.orgPermissions, generalOrganizationPermissions...)
// check project permissions
projectPermissions, err := checkProjectPermissions(cfg, client, domain, key, scopesConfig, org)
if err != nil {
return nil, err
}
info.projectPermissions = projectPermissions
return info, nil
}
func checkGeneralPermissions(cfg *config.Config, client *http.Client, domain, key string, scopesConfig *ScopesConfig) ([]Permission, error) {
return checkPermissions(cfg, client, domain, key, scopesConfig.GeneralScopes)
}
func checkOrganizationPermissions(
cfg *config.Config,
client *http.Client,
domain,
key string,
scopesConfig *ScopesConfig,
org *Organization,
) ([]Permission, error) {
return checkPermissions(cfg, client, domain, key, scopesConfig.OrganizationScopes, org.ID)
}
func checkProjectPermissions(
cfg *config.Config,
client *http.Client,
domain,
key string,
scopesConfig *ScopesConfig,
org *Organization,
) ([]ProjectPermissions, error) {
projectPermissions := make([]ProjectPermissions, 0)
for _, project := range org.Projects {
projectPermission := ProjectPermissions{
Project: &project,
}
permissions, err := checkPermissions(cfg, client, domain, key, scopesConfig.ProjectScopes, project.ID)
if err != nil {
return nil, err
}
projectPermission.Permissions = permissions
projectPermissions = append(projectPermissions, projectPermission)
}
return projectPermissions, nil
}
type User struct {
UUID string `json:"uuid"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Organization Organization `json:"organization"`
}
type Organization struct {
ID string `json:"id"`
Name string `json:"name"`
Projects []Project `json:"projects"`
}
type Project struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
// resolves the domain and user (if permission exists) by calling the /users/@me method for both US and EU domains
// if the response is 200 OK, it means the domain is valid and user:read permission is also there
// if the response is 403 Forbidden, it means the domain is valid but user:read permission is not there
// if the response is 401 Unauthorized, it means the domain is invalid
func resolveDomainAndUser(cfg *config.Config, client *http.Client, key string) (string, *User, error) {
domains := []string{USDomain, EUDomain}
for _, domain := range domains {
req, err := http.NewRequest(http.MethodGet, domain+"/api/users/@me/", nil)
if err != nil {
return "", nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return "", nil, err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
// domain is valid and user permission also exists
var userInfo User
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
return "", nil, err
}
return domain, &userInfo, nil
case http.StatusForbidden:
// domain is valid but user permission does not exist
return domain, nil, nil
case http.StatusUnauthorized:
// Key might not be valid of this domain
// Try the other domain
continue
default:
// unexpected status code
return "", nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
return "", nil, fmt.Errorf("invalid Posthog API key")
}
func getOrganization(cfg *config.Config, client *http.Client, domain string, key string) (*Organization, error) {
req, err := http.NewRequest(http.MethodGet, domain+"/api/organizations/@current/", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
var org Organization
if err := json.NewDecoder(resp.Body).Decode(&org); err != nil {
return nil, err
}
return &org, nil
case http.StatusForbidden:
return nil, nil
default:
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
func printUser(user User) {
color.Yellow("\n[i] User Info:")
color.Green("[i] Name: %s %s", user.FirstName, user.LastName)
color.Green("[i] Email: %s", user.Email)
color.Green("[i] ID: %s", user.UUID)
}
func printOrganizationPermissions(organization Organization, permissions []Permission) {
color.Yellow("\n[i] Organization Permissions:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Organization", "Permission"})
permissionsString := make([]string, len(permissions))
for i, permission := range permissions {
permissionsString[i], _ = permission.ToString()
}
t.AppendRow(table.Row{
color.GreenString(organization.Name),
color.GreenString(strings.Join(permissionsString, "\n")),
})
t.Render()
}
func printProjectPermissions(projectPermissions []ProjectPermissions) {
color.Yellow("\n[i] Project Permissions:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Project", "Permission"})
for _, projectPermission := range projectPermissions {
permissionsString := make([]string, len(projectPermission.Permissions))
for i, permission := range projectPermission.Permissions {
permissionsString[i], _ = permission.ToString()
}
t.AppendRow(table.Row{
color.GreenString(projectPermission.Project.Name),
color.GreenString(strings.Join(permissionsString, "\n")),
})
}
t.Render()
}
func printUnverifiedPermissions(permissions map[Permission]struct{}) {
color.Yellow("\n[i] Unverified Permissions:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Permission"})
for permission := range permissions {
permissionStr, _ := permission.ToString()
t.AppendRow(table.Row{color.YellowString(permissionStr)})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/posthog/posthog_test.go
================================================
package posthog
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid posthog api key",
key: testSecrets.MustGetField("POSTHOG_API_KEY"),
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/posthog/scopes.json
================================================
{
"general_scopes": [
{
"name": "organization",
"test": {
"read": {
"endpoint": "/api/organizations",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/organizations",
"method": "POST",
"valid_status_code": [
400
],
"invalid_status_code": [
403
]
}
}
}
],
"organization_scopes": [
{
"name": "batch_export",
"test": {
"read": {
"endpoint": "/api/organizations/%s/batch_exports",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/organizations/%s/batch_exports",
"method": "POST",
"valid_status_code": [
400
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "organization_member",
"test": {
"read": {
"endpoint": "/api/organizations/%s/members",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/organizations/%s/members/`nowaythiscanexist",
"method": "PATCH",
"valid_status_code": [
500
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "project",
"test": {
"read": {
"endpoint": "/api/organizations/%s/projects",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/organizations/%s/projects/`nowaythiscanexist",
"method": "DELETE",
"valid_status_code": [
400
],
"invalid_status_code": [
403
]
}
}
}
],
"project_scopes": [
{
"name": "action",
"test": {
"read": {
"endpoint": "/api/projects/%d/actions",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
},
"write": {
"endpoint": "/api/projects/%d/actions",
"method": "POST",
"valid_status_code": [500],
"invalid_status_code": [403]
}
}
},
{
"name": "activity_log",
"test": {
"read": {
"endpoint": "/api/projects/%d/activity_log",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/activity_log",
"method": "POST",
"valid_status_code": [
500
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "annotation",
"test": {
"read": {
"endpoint": "/api/projects/%d/annotations",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/annotations/`nowaythiscanexist",
"method": "PATCH",
"valid_status_code": [
404
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "cohort",
"test": {
"read": {
"endpoint": "/api/projects/%d/cohorts",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/cohorts/`nowaythiscanexist",
"method": "PATCH",
"valid_status_code": [
404
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "dashboard",
"test": {
"read": {
"endpoint": "/api/projects/%d/dashboards",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/dashboards/`nowaythiscanexist",
"method": "PATCH",
"valid_status_code": [
500
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "dashboard_template",
"test": {
"read": {
"endpoint": "/api/projects/%d/dashboard_templates",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/dashboard_templates/`nowaythiscanexist",
"method": "PATCH",
"valid_status_code": [
404
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "early_access_feature",
"test": {
"read": {
"endpoint": "/api/projects/%d/early_access_feature",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/early_access_feature",
"method": "POST",
"valid_status_code": [
400
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "event_definition",
"test": {
"read": {
"endpoint": "/api/projects/%d/event_definitions",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/event_definitions/`nowaythiscanexist",
"method": "PATCH",
"valid_status_code": [
500
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "experiment",
"test": {
"read": {
"endpoint": "/api/projects/%d/experiments",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/experiments",
"method": "POST",
"valid_status_code": [
400
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "export",
"test": {
"read": {
"endpoint": "/api/projects/%d/exports",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/exports",
"method": "POST",
"valid_status_code": [
400
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "feature_flag",
"test": {
"read": {
"endpoint": "/api/projects/%d/feature_flags",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/feature_flags",
"method": "POST",
"valid_status_code": [
400
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "group",
"test": {
"read": {
"endpoint": "/api/projects/%d/groups",
"method": "GET",
"valid_status_code": [
400
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/groups/update_property",
"method": "POST",
"valid_status_code": [
500
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "hog_function",
"test": {
"read": {
"endpoint": "/api/projects/%d/hog_functions",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/hog_functions/`nowaythiscanexist",
"method": "PATCH",
"valid_status_code": [
404
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "insight",
"test": {
"read": {
"endpoint": "/api/projects/%d/insights",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/insights/`nowaythiscanexist",
"method": "PATCH",
"valid_status_code": [
404
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "notebook",
"test": {
"read": {
"endpoint": "/api/projects/%d/notebooks",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/notebooks/`nowaythiscanexist",
"method": "PATCH",
"valid_status_code": [
404
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "person",
"test": {
"read": {
"endpoint": "/api/projects/%d/persons",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/persons/`nowaythiscanexist",
"method": "PATCH",
"valid_status_code": [
400
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "plugin",
"test": {
"read": {
"endpoint": "/api/projects/%d/plugin_configs",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/plugin_configs",
"method": "POST",
"valid_status_code": [
400
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "property_definition",
"test": {
"read": {
"endpoint": "/api/projects/%d/property_definitions",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/property_definitions/`nowaythiscanexist",
"method": "PATCH",
"valid_status_code": [
500
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "query",
"test": {
"read": {
"endpoint": "/api/projects/%d/query/`nowaythiscanexist",
"method": "GET",
"valid_status_code": [
404
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "session_recording",
"test": {
"read": {
"endpoint": "/api/projects/%d/session_recordings",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/session_recordings/`nowaythisexists",
"method": "PATCH",
"valid_status_code": [
404
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "session_recording_playlist",
"test": {
"read": {
"endpoint": "/api/projects/%d/session_recording_playlists",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/session_recording_playlists/`nowaythiscanexist",
"method": "PATCH",
"valid_status_code": [
404
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "subscription",
"test": {
"read": {
"endpoint": "/api/projects/%d/subscriptions",
"method": "GET",
"valid_status_code": [
200,
402
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/subscriptions/`nowaythiscanexist",
"method": "PATCH",
"valid_status_code": [
402,
404
],
"invalid_status_code": [
403
]
}
}
},
{
"name": "survey",
"test": {
"read": {
"endpoint": "/api/projects/%d/surveys",
"method": "GET",
"valid_status_code": [
200
],
"invalid_status_code": [
403
]
},
"write": {
"endpoint": "/api/projects/%d/surveys",
"method": "POST",
"valid_status_code": [
400
],
"invalid_status_code": [
403
]
}
}
}
]
}
================================================
FILE: pkg/analyzer/analyzers/postman/expected_output.json
================================================
{
"AnalyzerType": 13,
"Bindings": [
{
"Resource": {
"Name": "rendy",
"FullyQualifiedName": "rendyplayground@gmail.com",
"Type": "user",
"Metadata": {
"email": "rendyplayground@gmail.com",
"role": "user",
"team_domain": "",
"team_name": "",
"username": "rendyplayground"
},
"Parent": null
},
"Permission": {
"Value": "usage_data:view",
"Parent": null
}
},
{
"Resource": {
"Name": "rendy",
"FullyQualifiedName": "rendyplayground@gmail.com",
"Type": "user",
"Metadata": {
"email": "rendyplayground@gmail.com",
"role": "user",
"team_domain": "",
"team_name": "",
"username": "rendyplayground"
},
"Parent": null
},
"Permission": {
"Value": "team_workspaces:create",
"Parent": null
}
},
{
"Resource": {
"Name": "rendy",
"FullyQualifiedName": "rendyplayground@gmail.com",
"Type": "user",
"Metadata": {
"email": "rendyplayground@gmail.com",
"role": "user",
"team_domain": "",
"team_name": "",
"username": "rendyplayground"
},
"Parent": null
},
"Permission": {
"Value": "team_workspaces:view",
"Parent": null
}
}
],
"UnboundedResources": [
{
"Name": "My Workspace",
"FullyQualifiedName": "4d06fc0c-6402-4a26-857d-80787b10eabf",
"Type": "workspace",
"Metadata": {
"id": "4d06fc0c-6402-4a26-857d-80787b10eabf",
"type": "personal",
"visibility": "personal"
},
"Parent": null
}
],
"Metadata": null
}
================================================
FILE: pkg/analyzer/analyzers/postman/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package postman
import "errors"
type Permission int
const (
NoAccess Permission = iota
UserAdd Permission = iota
UserRemove Permission = iota
TeamAdminManage Permission = iota
TeamDevelopersManage Permission = iota
SsoManage Permission = iota
CustomDomainAdd Permission = iota
CustomDomainEdit Permission = iota
CustomDomainRemove Permission = iota
AuditLogsView Permission = iota
UsageDataView Permission = iota
BillingMembersManage Permission = iota
PaymentManage Permission = iota
PlanUpdate Permission = iota
TeamWorkspacesView Permission = iota
TeamWorkspacesCreate Permission = iota
TeamPublicProfileEnable Permission = iota
TeamPrivateApiNetworkManage Permission = iota
ParternerWorkspaceView Permission = iota
ParternerWorkspaceManage Permission = iota
ParternerWorkspaceVisibilityManage Permission = iota
PartnersManage Permission = iota
FlowAdd Permission = iota
FlowEdit Permission = iota
FlowRun Permission = iota
FlowPublish Permission = iota
)
var (
PermissionStrings = map[Permission]string{
UserAdd: "user:add",
UserRemove: "user:remove",
TeamAdminManage: "team_admin:manage",
TeamDevelopersManage: "team_developers:manage",
SsoManage: "sso:manage",
CustomDomainAdd: "custom_domain:add",
CustomDomainEdit: "custom_domain:edit",
CustomDomainRemove: "custom_domain:remove",
AuditLogsView: "audit_logs:view",
UsageDataView: "usage_data:view",
BillingMembersManage: "billing_members:manage",
PaymentManage: "payment:manage",
PlanUpdate: "plan:update",
TeamWorkspacesView: "team_workspaces:view",
TeamWorkspacesCreate: "team_workspaces:create",
TeamPublicProfileEnable: "team_public_profile:enable",
TeamPrivateApiNetworkManage: "team_private_api_network:manage",
ParternerWorkspaceView: "parterner_workspace:view",
ParternerWorkspaceManage: "parterner_workspace:manage",
ParternerWorkspaceVisibilityManage: "parterner_workspace_visibility:manage",
PartnersManage: "partners:manage",
FlowAdd: "flow:add",
FlowEdit: "flow:edit",
FlowRun: "flow:run",
FlowPublish: "flow:publish",
}
StringToPermission = map[string]Permission{
"user:add": UserAdd,
"user:remove": UserRemove,
"team_admin:manage": TeamAdminManage,
"team_developers:manage": TeamDevelopersManage,
"sso:manage": SsoManage,
"custom_domain:add": CustomDomainAdd,
"custom_domain:edit": CustomDomainEdit,
"custom_domain:remove": CustomDomainRemove,
"audit_logs:view": AuditLogsView,
"usage_data:view": UsageDataView,
"billing_members:manage": BillingMembersManage,
"payment:manage": PaymentManage,
"plan:update": PlanUpdate,
"team_workspaces:view": TeamWorkspacesView,
"team_workspaces:create": TeamWorkspacesCreate,
"team_public_profile:enable": TeamPublicProfileEnable,
"team_private_api_network:manage": TeamPrivateApiNetworkManage,
"parterner_workspace:view": ParternerWorkspaceView,
"parterner_workspace:manage": ParternerWorkspaceManage,
"parterner_workspace_visibility:manage": ParternerWorkspaceVisibilityManage,
"partners:manage": PartnersManage,
"flow:add": FlowAdd,
"flow:edit": FlowEdit,
"flow:run": FlowRun,
"flow:publish": FlowPublish,
}
PermissionIDs = map[Permission]int{
UserAdd: 0,
UserRemove: 1,
TeamAdminManage: 2,
TeamDevelopersManage: 3,
SsoManage: 4,
CustomDomainAdd: 5,
CustomDomainEdit: 6,
CustomDomainRemove: 7,
AuditLogsView: 8,
UsageDataView: 9,
BillingMembersManage: 10,
PaymentManage: 11,
PlanUpdate: 12,
TeamWorkspacesView: 13,
TeamWorkspacesCreate: 14,
TeamPublicProfileEnable: 15,
TeamPrivateApiNetworkManage: 16,
ParternerWorkspaceView: 17,
ParternerWorkspaceManage: 18,
ParternerWorkspaceVisibilityManage: 19,
PartnersManage: 20,
FlowAdd: 21,
FlowEdit: 22,
FlowRun: 23,
FlowPublish: 24,
}
IdToPermission = map[int]Permission{
0: UserAdd,
1: UserRemove,
2: TeamAdminManage,
3: TeamDevelopersManage,
4: SsoManage,
5: CustomDomainAdd,
6: CustomDomainEdit,
7: CustomDomainRemove,
8: AuditLogsView,
9: UsageDataView,
10: BillingMembersManage,
11: PaymentManage,
12: PlanUpdate,
13: TeamWorkspacesView,
14: TeamWorkspacesCreate,
15: TeamPublicProfileEnable,
16: TeamPrivateApiNetworkManage,
17: ParternerWorkspaceView,
18: ParternerWorkspaceManage,
19: ParternerWorkspaceVisibilityManage,
20: PartnersManage,
21: FlowAdd,
22: FlowEdit,
23: FlowRun,
24: FlowPublish,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/postman/permissions.yaml
================================================
permissions:
- user:add
- user:remove
- team_admin:manage
- team_developers:manage
- sso:manage
- custom_domain:add
- custom_domain:edit
- custom_domain:remove
- audit_logs:view
- usage_data:view
- billing_members:manage
- payment:manage
- plan:update
- team_workspaces:view
- team_workspaces:create
- team_public_profile:enable
- team_private_api_network:manage
- parterner_workspace:view
- parterner_workspace:manage
- parterner_workspace_visibility:manage
- partners:manage
- flow:add
- flow:edit
- flow:run
- flow:publish
================================================
FILE: pkg/analyzer/analyzers/postman/postman.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go postman
package postman
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypePostman }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, fmt.Errorf("missing key in credInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypePostman,
Metadata: nil,
UnboundedResources: []analyzers.Resource{},
}
resource := analyzers.Resource{
Name: info.User.User.FullName,
FullyQualifiedName: info.User.User.Email,
Type: "user",
Metadata: map[string]any{
"role": strings.Join(info.User.User.Roles, ","),
"username": info.User.User.Username,
"email": info.User.User.Email,
"team_name": info.User.User.TeamName,
"team_domain": info.User.User.TeamDomain,
},
}
permissions := bakePermissions(info.User.User.Roles)
// bind all permissions with resources
result.Bindings = analyzers.BindAllPermissions(resource, permissions...)
for _, workspace := range info.Workspace.Workspaces {
result.UnboundedResources = append(result.UnboundedResources, analyzers.Resource{
Name: workspace.Name,
FullyQualifiedName: workspace.ID,
Type: "workspace",
Metadata: map[string]any{
"id": workspace.ID,
"type": workspace.Type,
"visibility": workspace.Visibility,
},
})
}
return &result
}
func bakePermissions(roles []string) []analyzers.Permission {
permissionMap := map[Permission]struct{}{}
for _, role := range roles {
permissions, ok := rolePermission[role]
if !ok {
continue
}
for _, permission := range permissions {
permissionMap[permission] = struct{}{}
}
}
permissions := make([]analyzers.Permission, 0, len(permissionMap))
for perm := range permissionMap {
permStr, err := perm.ToString()
if err != nil {
continue
}
permissions = append(permissions, analyzers.Permission{
Value: permStr,
Parent: nil,
})
}
return permissions
}
type UserInfoJSON struct {
User struct {
Username string `json:"username"`
Email string `json:"email"`
FullName string `json:"fullName"`
Roles []string `json:"roles"`
TeamName string `json:"teamName"`
TeamDomain string `json:"teamDomain"`
} `json:"user"`
}
type WorkspaceJSON struct {
Workspaces []struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Visibility string `json:"visibility"`
} `json:"workspaces"`
}
func getUserInfo(cfg *config.Config, key string) (UserInfoJSON, error) {
var me UserInfoJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://api.getpostman.com/me", nil)
if err != nil {
return me, err
}
req.Header.Add("X-API-Key", key)
// send request
resp, err := client.Do(req)
if err != nil {
return me, err
}
// read response
defer resp.Body.Close()
// if status code is 200, decode response
if resp.StatusCode == 200 {
err = json.NewDecoder(resp.Body).Decode(&me)
}
return me, err
}
func getWorkspaces(cfg *config.Config, key string) (WorkspaceJSON, error) {
var workspaces WorkspaceJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://api.getpostman.com/workspaces", nil)
if err != nil {
return workspaces, err
}
req.Header.Add("X-API-Key", key)
// send request
resp, err := client.Do(req)
if err != nil {
return workspaces, err
}
// read response
defer resp.Body.Close()
// if status code is 200, decode response
if resp.StatusCode == 200 {
err = json.NewDecoder(resp.Body).Decode(&workspaces)
}
return workspaces, err
}
type SecretInfo struct {
User UserInfoJSON
Workspace WorkspaceJSON
WorkspaceError error
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
// ToDo: Add in logging
if cfg.LoggingEnabled {
color.Red("[x] Logging is not supported for this analyzer.")
return
}
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
color.Green("[!] Valid Postman API Key")
printUserInfo(info.User)
if info.WorkspaceError != nil {
color.Red("[x] Error Fetching Workspaces: %s", info.WorkspaceError.Error())
} else if len(info.Workspace.Workspaces) == 0 {
color.Red("[x] No Workspaces Found")
} else {
printWorkspaces(info.Workspace)
}
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
// validate key & get user info
me, err := getUserInfo(cfg, key)
if err != nil {
return nil, err
}
if me.User.Username == "" {
return nil, fmt.Errorf("Invalid Postman API Key")
}
// get workspaces, if there is error user with empty workspaces will be returned
workspaces, err := getWorkspaces(cfg, key)
return &SecretInfo{
User: me,
Workspace: workspaces,
WorkspaceError: err,
}, nil
}
func printUserInfo(me UserInfoJSON) {
color.Yellow("\n[i] User Information")
color.Green("Username: " + me.User.Username)
color.Green("Email: " + me.User.Email)
color.Green("Full Name: " + me.User.FullName)
color.Yellow("\n[i] Team Information")
color.Green("Name: " + me.User.TeamName)
color.Green("Domain: https://" + me.User.TeamDomain + ".postman.co")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "Permissions"})
for _, role := range me.User.Roles {
t.AppendRow([]interface{}{color.GreenString(role), color.GreenString(roleDescriptions[role])})
}
t.Render()
fmt.Println("Reference: https://learning.postman.com/docs/collaborating-in-postman/roles-and-permissions/#team-roles")
}
func printWorkspaces(workspaces WorkspaceJSON) {
color.Yellow("[i] Accessible Workspaces")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Workspace Name", "Type", "Visibility", "Link"})
for _, workspace := range workspaces.Workspaces {
t.AppendRow([]interface{}{color.GreenString(workspace.Name), color.GreenString(workspace.Type), color.GreenString(workspace.Visibility), color.GreenString("https://go.postman.co/workspaces/" + workspace.ID)})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/postman/postman_test.go
================================================
package postman
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid Postman key",
key: testSecrets.MustGetField("POSTMAN_TOKEN"),
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/postman/scopes.go
================================================
package postman
var roleDescriptions = map[string]string{
"super-admin": "(Enterprise Only) Manages everything within a team, including team settings, members, roles, and resources. This role can view and manage all elements in public, team, private, and personal workspaces. Super Admins can perform all actions that other roles can perform.",
"admin": "Manages team members and team settings. Can also view monitor metadata and run, pause, and resume monitors.",
"billing": "Manages team plan and payments. Billing roles can be granted by a Super Admin, Team Admin, or by a fellow team member with a Billing role.",
"user": "Has access to all team resources and workspaces.",
"community-manager": "(Pro & Enterprise Only) Manages the public visibility of workspaces and team profile.",
"partner-manager": "(Internal, Enterprise plans only) - Manages all Partner Workspaces within an organization. Controls Partner Workspace settings and visibility, and can send invites to partners.",
"partner": "(External, Professional and Enterprise plans only) - All partners are automatically granted the Partner role at the team level. Partners can only access the Partner Workspaces they've been invited to.",
"guest": "Views collections and sends requests in collections that have been shared with them. This role can't be directly assigned to a user.",
"flow-editor": "(Basic and Professional plans only) - Can create, edit, run, and publish Postman Flows.",
}
var rolePermission = map[string][]Permission{
"super-admin": {
UserAdd,
UserRemove,
TeamAdminManage,
TeamDevelopersManage,
SsoManage,
CustomDomainAdd,
CustomDomainEdit,
CustomDomainRemove,
AuditLogsView,
UsageDataView,
BillingMembersManage,
PaymentManage,
PlanUpdate,
TeamWorkspacesView,
TeamWorkspacesCreate,
TeamPublicProfileEnable,
TeamPrivateApiNetworkManage,
PartnersManage,
ParternerWorkspaceManage,
ParternerWorkspaceView,
ParternerWorkspaceVisibilityManage,
FlowAdd,
FlowEdit,
FlowRun,
FlowPublish,
},
"admin": {
UserAdd,
UserRemove,
TeamAdminManage,
TeamDevelopersManage,
SsoManage,
CustomDomainAdd,
CustomDomainEdit,
CustomDomainRemove,
AuditLogsView,
UsageDataView,
BillingMembersManage,
TeamPublicProfileEnable,
PartnersManage,
ParternerWorkspaceManage,
ParternerWorkspaceView,
ParternerWorkspaceVisibilityManage,
FlowAdd,
FlowEdit,
FlowRun,
FlowPublish,
},
"billing": {
UsageDataView,
BillingMembersManage,
PaymentManage,
PlanUpdate,
},
"user": {
UsageDataView,
TeamWorkspacesCreate,
TeamWorkspacesView,
},
"community-manager": {
CustomDomainAdd,
CustomDomainEdit,
AuditLogsView,
UsageDataView,
TeamWorkspacesView,
TeamWorkspacesCreate,
TeamPublicProfileEnable,
},
"partner-manager": {
PartnersManage,
ParternerWorkspaceManage,
ParternerWorkspaceView,
ParternerWorkspaceVisibilityManage,
},
"partner": {
ParternerWorkspaceView,
},
"guest": {
TeamWorkspacesView,
},
"flow-editor": {
FlowAdd,
FlowEdit,
FlowRun,
FlowPublish,
},
}
================================================
FILE: pkg/analyzer/analyzers/privatekey/expected_output.json
================================================
{"AnalyzerType":21,"Bindings":[],"UnboundedResources":[{"Name":"*.gruponu3.com","FullyQualifiedName":"/*.gruponu3.com","Type":"certificate","Metadata":null,"Parent":null},{"Name":"techautm.in","FullyQualifiedName":"/techautm.in","Type":"certificate","Metadata":null,"Parent":null}],"Metadata":null}
================================================
FILE: pkg/analyzer/analyzers/privatekey/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package privatekey
import "errors"
type Permission int
const (
Invalid Permission = iota
Digitalsignature Permission = iota
Nonrepudiation Permission = iota
Keyencipherment Permission = iota
Dataencipherment Permission = iota
Keyagreement Permission = iota
Certificatesigning Permission = iota
Crlsigning Permission = iota
Encipheronly Permission = iota
Decipheronly Permission = iota
Serverauth Permission = iota
Clientauth Permission = iota
Codesigning Permission = iota
Emailprotection Permission = iota
Timestamping Permission = iota
Ocspsigning Permission = iota
Clone Permission = iota
Push Permission = iota
)
var (
PermissionStrings = map[Permission]string{
Digitalsignature: "DigitalSignature",
Nonrepudiation: "NonRepudiation",
Keyencipherment: "KeyEncipherment",
Dataencipherment: "DataEncipherment",
Keyagreement: "KeyAgreement",
Certificatesigning: "CertificateSigning",
Crlsigning: "CRLSigning",
Encipheronly: "EncipherOnly",
Decipheronly: "DecipherOnly",
Serverauth: "ServerAuth",
Clientauth: "ClientAuth",
Codesigning: "CodeSigning",
Emailprotection: "EmailProtection",
Timestamping: "TimeStamping",
Ocspsigning: "OCSPSigning",
Clone: "Clone",
Push: "Push",
}
StringToPermission = map[string]Permission{
"DigitalSignature": Digitalsignature,
"NonRepudiation": Nonrepudiation,
"KeyEncipherment": Keyencipherment,
"DataEncipherment": Dataencipherment,
"KeyAgreement": Keyagreement,
"CertificateSigning": Certificatesigning,
"CRLSigning": Crlsigning,
"EncipherOnly": Encipheronly,
"DecipherOnly": Decipheronly,
"ServerAuth": Serverauth,
"ClientAuth": Clientauth,
"CodeSigning": Codesigning,
"EmailProtection": Emailprotection,
"TimeStamping": Timestamping,
"OCSPSigning": Ocspsigning,
"Clone": Clone,
"Push": Push,
}
PermissionIDs = map[Permission]int{
Digitalsignature: 1,
Nonrepudiation: 2,
Keyencipherment: 3,
Dataencipherment: 4,
Keyagreement: 5,
Certificatesigning: 6,
Crlsigning: 7,
Encipheronly: 8,
Decipheronly: 9,
Serverauth: 10,
Clientauth: 11,
Codesigning: 12,
Emailprotection: 13,
Timestamping: 14,
Ocspsigning: 15,
Clone: 16,
Push: 17,
}
IdToPermission = map[int]Permission{
1: Digitalsignature,
2: Nonrepudiation,
3: Keyencipherment,
4: Dataencipherment,
5: Keyagreement,
6: Certificatesigning,
7: Crlsigning,
8: Encipheronly,
9: Decipheronly,
10: Serverauth,
11: Clientauth,
12: Codesigning,
13: Emailprotection,
14: Timestamping,
15: Ocspsigning,
16: Clone,
17: Push,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/privatekey/permissions.yaml
================================================
permissions:
# TLS:
# KeyUsuage: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3
# ExtendedKeyUsage: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.12
- DigitalSignature
- NonRepudiation
- KeyEncipherment
- DataEncipherment
- KeyAgreement
- CertificateSigning
- CRLSigning
- EncipherOnly
- DecipherOnly
- ServerAuth
- ClientAuth
- CodeSigning
- EmailProtection
- TimeStamping
- OCSPSigning
# Github/Gitlab
- Clone
- Push
================================================
FILE: pkg/analyzer/analyzers/privatekey/privatekey.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go privatekey
package privatekey
import (
"errors"
"fmt"
"os"
"regexp"
"strings"
"sync"
"time"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/privatekey"
"golang.org/x/crypto/ssh"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypePrivateKey }
func (a Analyzer) Analyze(ctx context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
// token will be already normalized by the time it reaches here
token, ok := credInfo["token"]
if !ok {
return nil, errors.New("token not found in credInfo")
}
info, err := AnalyzePermissions(ctx, a.Cfg, token)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
type SecretInfo struct {
TLSCertificateResult *privatekey.DriftwoodResult
GithubUsername *string
GitlabUsername *string
}
func AnalyzePermissions(ctx context.Context, cfg *config.Config, token string) (*SecretInfo, error) {
var (
wg sync.WaitGroup
parsedKey any
err error
analyzerErrors = privatekey.NewVerificationErrors(3)
info = &SecretInfo{}
)
parsedKey, err = ssh.ParseRawPrivateKey([]byte(token))
if err != nil && strings.Contains(err.Error(), "private key is passphrase protected") {
// key is password protected
parsedKey, _, err = privatekey.Crack([]byte(token))
if err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
fingerprint, err := privatekey.FingerprintPEMKey(parsedKey)
if err != nil {
return nil, err
}
// Look up certificate information.
wg.Add(1)
go func() {
defer wg.Done()
data, err := analyzeFingerprint(ctx, fingerprint)
if err != nil {
analyzerErrors.Add(err)
} else {
info.TLSCertificateResult = data
}
}()
// Test SSH key against github.com
wg.Add(1)
go func() {
defer wg.Done()
user, err := analyzeGithubUser(ctx, parsedKey)
if err != nil {
analyzerErrors.Add(err)
} else if user != nil {
info.GithubUsername = user
}
}()
// Test SSH key against gitlab.com
wg.Add(1)
go func() {
defer wg.Done()
user, err := analyzeGitlabUser(ctx, parsedKey)
if err != nil {
analyzerErrors.Add(err)
} else if user != nil {
info.GitlabUsername = user
}
}()
wg.Wait()
if len(analyzerErrors.Errors) == 3 {
return nil, fmt.Errorf("analyzer failures: %s", strings.Join(analyzerErrors.Errors, ", "))
}
return info, nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
if cfg.LoggingEnabled {
color.Red("[x] Logging is not supported for this analyzer.")
return
}
token := privatekey.Normalize(key)
if len(token) < 64 {
color.Red("[x] Error: Invalid Private Key")
return
}
// key entered through command line may have spaces instead of newlines, replace them
token = replaceSpacesWithNewlines(token)
info, err := AnalyzePermissions(context.Background(), cfg, token)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
color.Green("[!] Valid Private Key\n\n")
if info.GithubUsername == nil && info.GitlabUsername == nil && info.TLSCertificateResult == nil {
color.Yellow("[i] Insufficient information returned from fingerprint analysis. No permissions found.")
return
}
if info.GithubUsername != nil {
color.Yellow("[i] GitHub Details:")
printUserInfo(*info.GithubUsername)
}
if info.GitlabUsername != nil {
color.Yellow("[i] GitLab Details:")
printUserInfo(*info.GitlabUsername)
}
if info.TLSCertificateResult != nil {
printTLSCertificateResult(info.TLSCertificateResult)
}
}
func printUserInfo(username string) {
color.Yellow("[i] Username: %s", username)
color.Yellow("[i] Permissions: %s\n\n", color.GreenString("Clone/Push"))
}
func printTLSCertificateResult(result *privatekey.DriftwoodResult) {
color.Yellow("[i] TLS Certificate Details:")
fmt.Print("\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(
table.Row{"Subject Key ID", "Subject Name", "Subject Organization", "Permissions", "Expiration Date", "Domains"})
green := color.New(color.FgGreen).SprintFunc()
for _, certificateResult := range result.CertificateResults {
t.AppendRow([]interface{}{
green(certificateResult.SubjectKeyID),
green(certificateResult.SubjectName),
green(strings.Join(certificateResult.SubjectOrganization, ", ")),
green(strings.Join(append(certificateResult.KeyUsages, certificateResult.ExtendedKeyUsages...), ", ")),
green(certificateResult.ExpirationTimestamp.Format(time.RFC3339)),
green(strings.Join(certificateResult.Domains, ", ")),
})
}
t.Render()
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypePrivateKey,
Metadata: nil,
Bindings: []analyzers.Binding{},
UnboundedResources: []analyzers.Resource{},
}
if info.TLSCertificateResult != nil {
bounded, unbounded := bakeTLSResources(info.TLSCertificateResult)
result.Bindings = append(result.Bindings, bounded...)
result.UnboundedResources = append(result.UnboundedResources, unbounded...)
}
if info.GithubUsername != nil {
result.Bindings = append(result.Bindings, bakeGithubResources(info.GithubUsername)...)
}
if info.GitlabUsername != nil {
result.Bindings = append(result.Bindings, bakeGitlabResources(info.GitlabUsername)...)
}
return &result
}
func bakeGithubResources(username *string) []analyzers.Binding {
resource := &analyzers.Resource{
Name: *username,
FullyQualifiedName: fmt.Sprintf("github.com/user/%s", *username),
Type: "user", // always user ???
}
permissions := []analyzers.Permission{
{Value: PermissionStrings[Clone], Parent: nil},
{Value: PermissionStrings[Push], Parent: nil},
}
return analyzers.BindAllPermissions(*resource, permissions...)
}
func bakeGitlabResources(username *string) []analyzers.Binding {
resource := &analyzers.Resource{
Name: *username,
FullyQualifiedName: fmt.Sprintf("gitlab.com/user/%s", *username),
Type: "user", // always user ???
}
permissions := []analyzers.Permission{
{Value: PermissionStrings[Clone], Parent: nil},
{Value: PermissionStrings[Push], Parent: nil},
}
return analyzers.BindAllPermissions(*resource, permissions...)
}
func bakeTLSResources(result *privatekey.DriftwoodResult) ([]analyzers.Binding, []analyzers.Resource) {
unboundedResources := make([]analyzers.Resource, 0, len(result.CertificateResults))
boundedResources := make([]analyzers.Binding, 0, len(result.CertificateResults))
// iterate result.CertificateResults
for _, cert := range result.CertificateResults {
if cert.SubjectName == "" && cert.SubjectKeyID == "" {
continue
}
resource := &analyzers.Resource{
Name: cert.SubjectName,
FullyQualifiedName: fmt.Sprintf("%s/%s", cert.SubjectKeyID, cert.SubjectName),
Type: "certificate",
}
certPermissions := append(cert.KeyUsages, cert.ExtendedKeyUsages...)
permissions := make([]analyzers.Permission, 0, len(certPermissions))
for _, perm := range certPermissions {
perm, ok := StringToPermission[perm]
if !ok {
continue
}
permissions = append(permissions, analyzers.Permission{
Value: PermissionStrings[perm],
Parent: nil,
})
}
if len(permissions) > 0 {
// bind all permissions with resources
boundedResources = append(boundedResources, analyzers.BindAllPermissions(*resource, permissions...)...)
} else {
unboundedResources = append(unboundedResources, *resource)
}
}
return boundedResources, unboundedResources
}
func analyzeFingerprint(ctx context.Context, fingerprint string) (*privatekey.DriftwoodResult, error) {
result, err := privatekey.LookupFingerprint(ctx, fingerprint)
if err != nil {
return nil, err
}
if len(result.CertificateResults) == 0 {
return nil, nil
}
return result, nil
}
func analyzeGithubUser(ctx context.Context, parsedKey any) (*string, error) {
return privatekey.VerifyGitHubUser(ctx, parsedKey)
}
func analyzeGitlabUser(ctx context.Context, parsedKey any) (*string, error) {
return privatekey.VerifyGitLabUser(ctx, parsedKey)
}
// replaceSpacesWithNewlines extracts the base64 part, replaces spaces with newlines if needed, and reconstructs the key.
func replaceSpacesWithNewlines(privateKey string) string {
// Regex pattern to extract the key content
re := regexp.MustCompile(`(?i)(-----\s*BEGIN[ A-Z0-9_-]*PRIVATE KEY\s*-----)\s*([\s\S]*?)\s*(-----\s*END[ A-Z0-9_-]*PRIVATE KEY\s*-----)`)
// Find matches
matches := re.FindStringSubmatch(privateKey)
if len(matches) != 4 {
// no need to process
return privateKey
}
header := matches[1] // BEGIN line
base64Part := matches[2] // Base64 content
footer := matches[3] // END line
// Replace spaces with newlines
formattedBase64 := strings.ReplaceAll(base64Part, " ", "\n")
// Reconstruct the private key
return fmt.Sprintf("%s\n%s\n%s", header, formattedBase64, footer)
}
================================================
FILE: pkg/analyzer/analyzers/privatekey/privatekey_test.go
================================================
package privatekey
import (
_ "embed"
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
privateKey := testSecrets.MustGetField("PRIVATEKEY_TLS")
tests := []struct {
name string
key string
storeUrl string
want string
wantErr bool
}{
{
name: "valid TLS key",
key: privateKey,
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/sendgrid/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package sendgrid
import "errors"
type Permission int
const (
Invalid Permission = iota
AccessSettingsActivityRead Permission = iota
AccessSettingsWhitelistCreate Permission = iota
AccessSettingsWhitelistDelete Permission = iota
AccessSettingsWhitelistRead Permission = iota
AccessSettingsWhitelistUpdate Permission = iota
AlertsCreate Permission = iota
AlertsDelete Permission = iota
AlertsRead Permission = iota
AlertsUpdate Permission = iota
ApiKeysCreate Permission = iota
ApiKeysDelete Permission = iota
ApiKeysRead Permission = iota
ApiKeysUpdate Permission = iota
AsmGroupsCreate Permission = iota
AsmGroupsDelete Permission = iota
AsmGroupsRead Permission = iota
AsmGroupsUpdate Permission = iota
BillingCreate Permission = iota
BillingDelete Permission = iota
BillingRead Permission = iota
BillingUpdate Permission = iota
BrowsersStatsRead Permission = iota
CategoriesCreate Permission = iota
CategoriesDelete Permission = iota
CategoriesRead Permission = iota
CategoriesStatsRead Permission = iota
CategoriesStatsSumsRead Permission = iota
CategoriesUpdate Permission = iota
ClientsDesktopStatsRead Permission = iota
ClientsPhoneStatsRead Permission = iota
ClientsStatsRead Permission = iota
ClientsTabletStatsRead Permission = iota
ClientsWebmailStatsRead Permission = iota
DevicesStatsRead Permission = iota
EmailActivityRead Permission = iota
GeoStatsRead Permission = iota
IpsAssignedRead Permission = iota
IpsPoolsCreate Permission = iota
IpsPoolsDelete Permission = iota
IpsPoolsIpsCreate Permission = iota
IpsPoolsIpsDelete Permission = iota
IpsPoolsIpsRead Permission = iota
IpsPoolsIpsUpdate Permission = iota
IpsPoolsRead Permission = iota
IpsPoolsUpdate Permission = iota
IpsRead Permission = iota
IpsWarmupCreate Permission = iota
IpsWarmupDelete Permission = iota
IpsWarmupRead Permission = iota
IpsWarmupUpdate Permission = iota
MailSettingsAddressWhitelistRead Permission = iota
MailSettingsAddressWhitelistUpdate Permission = iota
MailSettingsBouncePurgeRead Permission = iota
MailSettingsBouncePurgeUpdate Permission = iota
MailSettingsFooterRead Permission = iota
MailSettingsFooterUpdate Permission = iota
MailSettingsForwardBounceRead Permission = iota
MailSettingsForwardBounceUpdate Permission = iota
MailSettingsForwardSpamRead Permission = iota
MailSettingsForwardSpamUpdate Permission = iota
MailSettingsPlainContentRead Permission = iota
MailSettingsPlainContentUpdate Permission = iota
MailSettingsRead Permission = iota
MailSettingsTemplateRead Permission = iota
MailSettingsTemplateUpdate Permission = iota
MailBatchCreate Permission = iota
MailBatchDelete Permission = iota
MailBatchRead Permission = iota
MailBatchUpdate Permission = iota
MailSend Permission = iota
MailboxProvidersStatsRead Permission = iota
MarketingCampaignsCreate Permission = iota
MarketingCampaignsDelete Permission = iota
MarketingCampaignsRead Permission = iota
MarketingCampaignsUpdate Permission = iota
PartnerSettingsNewRelicRead Permission = iota
PartnerSettingsNewRelicUpdate Permission = iota
PartnerSettingsRead Permission = iota
StatsGlobalRead Permission = iota
StatsRead Permission = iota
SubusersCreate Permission = iota
SubusersCreditsCreate Permission = iota
SubusersCreditsDelete Permission = iota
SubusersCreditsRead Permission = iota
SubusersCreditsRemainingCreate Permission = iota
SubusersCreditsRemainingDelete Permission = iota
SubusersCreditsRemainingRead Permission = iota
SubusersCreditsRemainingUpdate Permission = iota
SubusersCreditsUpdate Permission = iota
SubusersDelete Permission = iota
SubusersMonitorCreate Permission = iota
SubusersMonitorDelete Permission = iota
SubusersMonitorRead Permission = iota
SubusersMonitorUpdate Permission = iota
SubusersRead Permission = iota
SubusersReputationsRead Permission = iota
SubusersStatsMonthlyRead Permission = iota
SubusersStatsRead Permission = iota
SubusersStatsSumsRead Permission = iota
SubusersSummaryRead Permission = iota
SubusersUpdate Permission = iota
SuppressionBlocksCreate Permission = iota
SuppressionBlocksDelete Permission = iota
SuppressionBlocksRead Permission = iota
SuppressionBlocksUpdate Permission = iota
SuppressionBouncesCreate Permission = iota
SuppressionBouncesDelete Permission = iota
SuppressionBouncesRead Permission = iota
SuppressionBouncesUpdate Permission = iota
SuppressionCreate Permission = iota
SuppressionDelete Permission = iota
SuppressionInvalidEmailsCreate Permission = iota
SuppressionInvalidEmailsDelete Permission = iota
SuppressionInvalidEmailsRead Permission = iota
SuppressionInvalidEmailsUpdate Permission = iota
SuppressionRead Permission = iota
SuppressionSpamReportsCreate Permission = iota
SuppressionSpamReportsDelete Permission = iota
SuppressionSpamReportsRead Permission = iota
SuppressionSpamReportsUpdate Permission = iota
SuppressionUnsubscribesCreate Permission = iota
SuppressionUnsubscribesDelete Permission = iota
SuppressionUnsubscribesRead Permission = iota
SuppressionUnsubscribesUpdate Permission = iota
SuppressionUpdate Permission = iota
TeammatesCreate Permission = iota
TeammatesRead Permission = iota
TeammatesUpdate Permission = iota
TeammatesDelete Permission = iota
TemplatesCreate Permission = iota
TemplatesDelete Permission = iota
TemplatesRead Permission = iota
TemplatesUpdate Permission = iota
TemplatesVersionsActivateCreate Permission = iota
TemplatesVersionsActivateDelete Permission = iota
TemplatesVersionsActivateRead Permission = iota
TemplatesVersionsActivateUpdate Permission = iota
TemplatesVersionsCreate Permission = iota
TemplatesVersionsDelete Permission = iota
TemplatesVersionsRead Permission = iota
TemplatesVersionsUpdate Permission = iota
TrackingSettingsClickRead Permission = iota
TrackingSettingsClickUpdate Permission = iota
TrackingSettingsGoogleAnalyticsRead Permission = iota
TrackingSettingsGoogleAnalyticsUpdate Permission = iota
TrackingSettingsOpenRead Permission = iota
TrackingSettingsOpenUpdate Permission = iota
TrackingSettingsRead Permission = iota
TrackingSettingsSubscriptionRead Permission = iota
TrackingSettingsSubscriptionUpdate Permission = iota
UserAccountRead Permission = iota
UserCreditsRead Permission = iota
UserEmailCreate Permission = iota
UserEmailDelete Permission = iota
UserEmailRead Permission = iota
UserEmailUpdate Permission = iota
UserMultifactorAuthenticationCreate Permission = iota
UserMultifactorAuthenticationDelete Permission = iota
UserMultifactorAuthenticationRead Permission = iota
UserMultifactorAuthenticationUpdate Permission = iota
UserPasswordRead Permission = iota
UserPasswordUpdate Permission = iota
UserProfileRead Permission = iota
UserProfileUpdate Permission = iota
UserScheduledSendsCreate Permission = iota
UserScheduledSendsDelete Permission = iota
UserScheduledSendsRead Permission = iota
UserScheduledSendsUpdate Permission = iota
UserSettingsEnforcedTlsRead Permission = iota
UserSettingsEnforcedTlsUpdate Permission = iota
UserTimezoneRead Permission = iota
UserUsernameRead Permission = iota
UserUsernameUpdate Permission = iota
UserWebhooksEventSettingsRead Permission = iota
UserWebhooksEventSettingsUpdate Permission = iota
UserWebhooksEventTestCreate Permission = iota
UserWebhooksEventTestRead Permission = iota
UserWebhooksEventTestUpdate Permission = iota
UserWebhooksParseSettingsCreate Permission = iota
UserWebhooksParseSettingsDelete Permission = iota
UserWebhooksParseSettingsRead Permission = iota
UserWebhooksParseSettingsUpdate Permission = iota
UserWebhooksParseStatsRead Permission = iota
WhitelabelCreate Permission = iota
WhitelabelDelete Permission = iota
WhitelabelRead Permission = iota
WhitelabelUpdate Permission = iota
)
var (
PermissionStrings = map[Permission]string{
AccessSettingsActivityRead: "access_settings.activity.read",
AccessSettingsWhitelistCreate: "access_settings.whitelist.create",
AccessSettingsWhitelistDelete: "access_settings.whitelist.delete",
AccessSettingsWhitelistRead: "access_settings.whitelist.read",
AccessSettingsWhitelistUpdate: "access_settings.whitelist.update",
AlertsCreate: "alerts.create",
AlertsDelete: "alerts.delete",
AlertsRead: "alerts.read",
AlertsUpdate: "alerts.update",
ApiKeysCreate: "api_keys.create",
ApiKeysDelete: "api_keys.delete",
ApiKeysRead: "api_keys.read",
ApiKeysUpdate: "api_keys.update",
AsmGroupsCreate: "asm.groups.create",
AsmGroupsDelete: "asm.groups.delete",
AsmGroupsRead: "asm.groups.read",
AsmGroupsUpdate: "asm.groups.update",
BillingCreate: "billing.create",
BillingDelete: "billing.delete",
BillingRead: "billing.read",
BillingUpdate: "billing.update",
BrowsersStatsRead: "browsers.stats.read",
CategoriesCreate: "categories.create",
CategoriesDelete: "categories.delete",
CategoriesRead: "categories.read",
CategoriesStatsRead: "categories.stats.read",
CategoriesStatsSumsRead: "categories.stats.sums.read",
CategoriesUpdate: "categories.update",
ClientsDesktopStatsRead: "clients.desktop.stats.read",
ClientsPhoneStatsRead: "clients.phone.stats.read",
ClientsStatsRead: "clients.stats.read",
ClientsTabletStatsRead: "clients.tablet.stats.read",
ClientsWebmailStatsRead: "clients.webmail.stats.read",
DevicesStatsRead: "devices.stats.read",
EmailActivityRead: "email_activity.read",
GeoStatsRead: "geo.stats.read",
IpsAssignedRead: "ips.assigned.read",
IpsPoolsCreate: "ips.pools.create",
IpsPoolsDelete: "ips.pools.delete",
IpsPoolsIpsCreate: "ips.pools.ips.create",
IpsPoolsIpsDelete: "ips.pools.ips.delete",
IpsPoolsIpsRead: "ips.pools.ips.read",
IpsPoolsIpsUpdate: "ips.pools.ips.update",
IpsPoolsRead: "ips.pools.read",
IpsPoolsUpdate: "ips.pools.update",
IpsRead: "ips.read",
IpsWarmupCreate: "ips.warmup.create",
IpsWarmupDelete: "ips.warmup.delete",
IpsWarmupRead: "ips.warmup.read",
IpsWarmupUpdate: "ips.warmup.update",
MailSettingsAddressWhitelistRead: "mail_settings.address_whitelist.read",
MailSettingsAddressWhitelistUpdate: "mail_settings.address_whitelist.update",
MailSettingsBouncePurgeRead: "mail_settings.bounce_purge.read",
MailSettingsBouncePurgeUpdate: "mail_settings.bounce_purge.update",
MailSettingsFooterRead: "mail_settings.footer.read",
MailSettingsFooterUpdate: "mail_settings.footer.update",
MailSettingsForwardBounceRead: "mail_settings.forward_bounce.read",
MailSettingsForwardBounceUpdate: "mail_settings.forward_bounce.update",
MailSettingsForwardSpamRead: "mail_settings.forward_spam.read",
MailSettingsForwardSpamUpdate: "mail_settings.forward_spam.update",
MailSettingsPlainContentRead: "mail_settings.plain_content.read",
MailSettingsPlainContentUpdate: "mail_settings.plain_content.update",
MailSettingsRead: "mail_settings.read",
MailSettingsTemplateRead: "mail_settings.template.read",
MailSettingsTemplateUpdate: "mail_settings.template.update",
MailBatchCreate: "mail.batch.create",
MailBatchDelete: "mail.batch.delete",
MailBatchRead: "mail.batch.read",
MailBatchUpdate: "mail.batch.update",
MailSend: "mail.send",
MailboxProvidersStatsRead: "mailbox_providers.stats.read",
MarketingCampaignsCreate: "marketing_campaigns.create",
MarketingCampaignsDelete: "marketing_campaigns.delete",
MarketingCampaignsRead: "marketing_campaigns.read",
MarketingCampaignsUpdate: "marketing_campaigns.update",
PartnerSettingsNewRelicRead: "partner_settings.new_relic.read",
PartnerSettingsNewRelicUpdate: "partner_settings.new_relic.update",
PartnerSettingsRead: "partner_settings.read",
StatsGlobalRead: "stats.global.read",
StatsRead: "stats.read",
SubusersCreate: "subusers.create",
SubusersCreditsCreate: "subusers.credits.create",
SubusersCreditsDelete: "subusers.credits.delete",
SubusersCreditsRead: "subusers.credits.read",
SubusersCreditsRemainingCreate: "subusers.credits.remaining.create",
SubusersCreditsRemainingDelete: "subusers.credits.remaining.delete",
SubusersCreditsRemainingRead: "subusers.credits.remaining.read",
SubusersCreditsRemainingUpdate: "subusers.credits.remaining.update",
SubusersCreditsUpdate: "subusers.credits.update",
SubusersDelete: "subusers.delete",
SubusersMonitorCreate: "subusers.monitor.create",
SubusersMonitorDelete: "subusers.monitor.delete",
SubusersMonitorRead: "subusers.monitor.read",
SubusersMonitorUpdate: "subusers.monitor.update",
SubusersRead: "subusers.read",
SubusersReputationsRead: "subusers.reputations.read",
SubusersStatsMonthlyRead: "subusers.stats.monthly.read",
SubusersStatsRead: "subusers.stats.read",
SubusersStatsSumsRead: "subusers.stats.sums.read",
SubusersSummaryRead: "subusers.summary.read",
SubusersUpdate: "subusers.update",
SuppressionBlocksCreate: "suppression.blocks.create",
SuppressionBlocksDelete: "suppression.blocks.delete",
SuppressionBlocksRead: "suppression.blocks.read",
SuppressionBlocksUpdate: "suppression.blocks.update",
SuppressionBouncesCreate: "suppression.bounces.create",
SuppressionBouncesDelete: "suppression.bounces.delete",
SuppressionBouncesRead: "suppression.bounces.read",
SuppressionBouncesUpdate: "suppression.bounces.update",
SuppressionCreate: "suppression.create",
SuppressionDelete: "suppression.delete",
SuppressionInvalidEmailsCreate: "suppression.invalid_emails.create",
SuppressionInvalidEmailsDelete: "suppression.invalid_emails.delete",
SuppressionInvalidEmailsRead: "suppression.invalid_emails.read",
SuppressionInvalidEmailsUpdate: "suppression.invalid_emails.update",
SuppressionRead: "suppression.read",
SuppressionSpamReportsCreate: "suppression.spam_reports.create",
SuppressionSpamReportsDelete: "suppression.spam_reports.delete",
SuppressionSpamReportsRead: "suppression.spam_reports.read",
SuppressionSpamReportsUpdate: "suppression.spam_reports.update",
SuppressionUnsubscribesCreate: "suppression.unsubscribes.create",
SuppressionUnsubscribesDelete: "suppression.unsubscribes.delete",
SuppressionUnsubscribesRead: "suppression.unsubscribes.read",
SuppressionUnsubscribesUpdate: "suppression.unsubscribes.update",
SuppressionUpdate: "suppression.update",
TeammatesCreate: "teammates.create",
TeammatesRead: "teammates.read",
TeammatesUpdate: "teammates.update",
TeammatesDelete: "teammates.delete",
TemplatesCreate: "templates.create",
TemplatesDelete: "templates.delete",
TemplatesRead: "templates.read",
TemplatesUpdate: "templates.update",
TemplatesVersionsActivateCreate: "templates.versions.activate.create",
TemplatesVersionsActivateDelete: "templates.versions.activate.delete",
TemplatesVersionsActivateRead: "templates.versions.activate.read",
TemplatesVersionsActivateUpdate: "templates.versions.activate.update",
TemplatesVersionsCreate: "templates.versions.create",
TemplatesVersionsDelete: "templates.versions.delete",
TemplatesVersionsRead: "templates.versions.read",
TemplatesVersionsUpdate: "templates.versions.update",
TrackingSettingsClickRead: "tracking_settings.click.read",
TrackingSettingsClickUpdate: "tracking_settings.click.update",
TrackingSettingsGoogleAnalyticsRead: "tracking_settings.google_analytics.read",
TrackingSettingsGoogleAnalyticsUpdate: "tracking_settings.google_analytics.update",
TrackingSettingsOpenRead: "tracking_settings.open.read",
TrackingSettingsOpenUpdate: "tracking_settings.open.update",
TrackingSettingsRead: "tracking_settings.read",
TrackingSettingsSubscriptionRead: "tracking_settings.subscription.read",
TrackingSettingsSubscriptionUpdate: "tracking_settings.subscription.update",
UserAccountRead: "user.account.read",
UserCreditsRead: "user.credits.read",
UserEmailCreate: "user.email.create",
UserEmailDelete: "user.email.delete",
UserEmailRead: "user.email.read",
UserEmailUpdate: "user.email.update",
UserMultifactorAuthenticationCreate: "user.multifactor_authentication.create",
UserMultifactorAuthenticationDelete: "user.multifactor_authentication.delete",
UserMultifactorAuthenticationRead: "user.multifactor_authentication.read",
UserMultifactorAuthenticationUpdate: "user.multifactor_authentication.update",
UserPasswordRead: "user.password.read",
UserPasswordUpdate: "user.password.update",
UserProfileRead: "user.profile.read",
UserProfileUpdate: "user.profile.update",
UserScheduledSendsCreate: "user.scheduled_sends.create",
UserScheduledSendsDelete: "user.scheduled_sends.delete",
UserScheduledSendsRead: "user.scheduled_sends.read",
UserScheduledSendsUpdate: "user.scheduled_sends.update",
UserSettingsEnforcedTlsRead: "user.settings.enforced_tls.read",
UserSettingsEnforcedTlsUpdate: "user.settings.enforced_tls.update",
UserTimezoneRead: "user.timezone.read",
UserUsernameRead: "user.username.read",
UserUsernameUpdate: "user.username.update",
UserWebhooksEventSettingsRead: "user.webhooks.event.settings.read",
UserWebhooksEventSettingsUpdate: "user.webhooks.event.settings.update",
UserWebhooksEventTestCreate: "user.webhooks.event.test.create",
UserWebhooksEventTestRead: "user.webhooks.event.test.read",
UserWebhooksEventTestUpdate: "user.webhooks.event.test.update",
UserWebhooksParseSettingsCreate: "user.webhooks.parse.settings.create",
UserWebhooksParseSettingsDelete: "user.webhooks.parse.settings.delete",
UserWebhooksParseSettingsRead: "user.webhooks.parse.settings.read",
UserWebhooksParseSettingsUpdate: "user.webhooks.parse.settings.update",
UserWebhooksParseStatsRead: "user.webhooks.parse.stats.read",
WhitelabelCreate: "whitelabel.create",
WhitelabelDelete: "whitelabel.delete",
WhitelabelRead: "whitelabel.read",
WhitelabelUpdate: "whitelabel.update",
}
StringToPermission = map[string]Permission{
"access_settings.activity.read": AccessSettingsActivityRead,
"access_settings.whitelist.create": AccessSettingsWhitelistCreate,
"access_settings.whitelist.delete": AccessSettingsWhitelistDelete,
"access_settings.whitelist.read": AccessSettingsWhitelistRead,
"access_settings.whitelist.update": AccessSettingsWhitelistUpdate,
"alerts.create": AlertsCreate,
"alerts.delete": AlertsDelete,
"alerts.read": AlertsRead,
"alerts.update": AlertsUpdate,
"api_keys.create": ApiKeysCreate,
"api_keys.delete": ApiKeysDelete,
"api_keys.read": ApiKeysRead,
"api_keys.update": ApiKeysUpdate,
"asm.groups.create": AsmGroupsCreate,
"asm.groups.delete": AsmGroupsDelete,
"asm.groups.read": AsmGroupsRead,
"asm.groups.update": AsmGroupsUpdate,
"billing.create": BillingCreate,
"billing.delete": BillingDelete,
"billing.read": BillingRead,
"billing.update": BillingUpdate,
"browsers.stats.read": BrowsersStatsRead,
"categories.create": CategoriesCreate,
"categories.delete": CategoriesDelete,
"categories.read": CategoriesRead,
"categories.stats.read": CategoriesStatsRead,
"categories.stats.sums.read": CategoriesStatsSumsRead,
"categories.update": CategoriesUpdate,
"clients.desktop.stats.read": ClientsDesktopStatsRead,
"clients.phone.stats.read": ClientsPhoneStatsRead,
"clients.stats.read": ClientsStatsRead,
"clients.tablet.stats.read": ClientsTabletStatsRead,
"clients.webmail.stats.read": ClientsWebmailStatsRead,
"devices.stats.read": DevicesStatsRead,
"email_activity.read": EmailActivityRead,
"geo.stats.read": GeoStatsRead,
"ips.assigned.read": IpsAssignedRead,
"ips.pools.create": IpsPoolsCreate,
"ips.pools.delete": IpsPoolsDelete,
"ips.pools.ips.create": IpsPoolsIpsCreate,
"ips.pools.ips.delete": IpsPoolsIpsDelete,
"ips.pools.ips.read": IpsPoolsIpsRead,
"ips.pools.ips.update": IpsPoolsIpsUpdate,
"ips.pools.read": IpsPoolsRead,
"ips.pools.update": IpsPoolsUpdate,
"ips.read": IpsRead,
"ips.warmup.create": IpsWarmupCreate,
"ips.warmup.delete": IpsWarmupDelete,
"ips.warmup.read": IpsWarmupRead,
"ips.warmup.update": IpsWarmupUpdate,
"mail_settings.address_whitelist.read": MailSettingsAddressWhitelistRead,
"mail_settings.address_whitelist.update": MailSettingsAddressWhitelistUpdate,
"mail_settings.bounce_purge.read": MailSettingsBouncePurgeRead,
"mail_settings.bounce_purge.update": MailSettingsBouncePurgeUpdate,
"mail_settings.footer.read": MailSettingsFooterRead,
"mail_settings.footer.update": MailSettingsFooterUpdate,
"mail_settings.forward_bounce.read": MailSettingsForwardBounceRead,
"mail_settings.forward_bounce.update": MailSettingsForwardBounceUpdate,
"mail_settings.forward_spam.read": MailSettingsForwardSpamRead,
"mail_settings.forward_spam.update": MailSettingsForwardSpamUpdate,
"mail_settings.plain_content.read": MailSettingsPlainContentRead,
"mail_settings.plain_content.update": MailSettingsPlainContentUpdate,
"mail_settings.read": MailSettingsRead,
"mail_settings.template.read": MailSettingsTemplateRead,
"mail_settings.template.update": MailSettingsTemplateUpdate,
"mail.batch.create": MailBatchCreate,
"mail.batch.delete": MailBatchDelete,
"mail.batch.read": MailBatchRead,
"mail.batch.update": MailBatchUpdate,
"mail.send": MailSend,
"mailbox_providers.stats.read": MailboxProvidersStatsRead,
"marketing_campaigns.create": MarketingCampaignsCreate,
"marketing_campaigns.delete": MarketingCampaignsDelete,
"marketing_campaigns.read": MarketingCampaignsRead,
"marketing_campaigns.update": MarketingCampaignsUpdate,
"partner_settings.new_relic.read": PartnerSettingsNewRelicRead,
"partner_settings.new_relic.update": PartnerSettingsNewRelicUpdate,
"partner_settings.read": PartnerSettingsRead,
"stats.global.read": StatsGlobalRead,
"stats.read": StatsRead,
"subusers.create": SubusersCreate,
"subusers.credits.create": SubusersCreditsCreate,
"subusers.credits.delete": SubusersCreditsDelete,
"subusers.credits.read": SubusersCreditsRead,
"subusers.credits.remaining.create": SubusersCreditsRemainingCreate,
"subusers.credits.remaining.delete": SubusersCreditsRemainingDelete,
"subusers.credits.remaining.read": SubusersCreditsRemainingRead,
"subusers.credits.remaining.update": SubusersCreditsRemainingUpdate,
"subusers.credits.update": SubusersCreditsUpdate,
"subusers.delete": SubusersDelete,
"subusers.monitor.create": SubusersMonitorCreate,
"subusers.monitor.delete": SubusersMonitorDelete,
"subusers.monitor.read": SubusersMonitorRead,
"subusers.monitor.update": SubusersMonitorUpdate,
"subusers.read": SubusersRead,
"subusers.reputations.read": SubusersReputationsRead,
"subusers.stats.monthly.read": SubusersStatsMonthlyRead,
"subusers.stats.read": SubusersStatsRead,
"subusers.stats.sums.read": SubusersStatsSumsRead,
"subusers.summary.read": SubusersSummaryRead,
"subusers.update": SubusersUpdate,
"suppression.blocks.create": SuppressionBlocksCreate,
"suppression.blocks.delete": SuppressionBlocksDelete,
"suppression.blocks.read": SuppressionBlocksRead,
"suppression.blocks.update": SuppressionBlocksUpdate,
"suppression.bounces.create": SuppressionBouncesCreate,
"suppression.bounces.delete": SuppressionBouncesDelete,
"suppression.bounces.read": SuppressionBouncesRead,
"suppression.bounces.update": SuppressionBouncesUpdate,
"suppression.create": SuppressionCreate,
"suppression.delete": SuppressionDelete,
"suppression.invalid_emails.create": SuppressionInvalidEmailsCreate,
"suppression.invalid_emails.delete": SuppressionInvalidEmailsDelete,
"suppression.invalid_emails.read": SuppressionInvalidEmailsRead,
"suppression.invalid_emails.update": SuppressionInvalidEmailsUpdate,
"suppression.read": SuppressionRead,
"suppression.spam_reports.create": SuppressionSpamReportsCreate,
"suppression.spam_reports.delete": SuppressionSpamReportsDelete,
"suppression.spam_reports.read": SuppressionSpamReportsRead,
"suppression.spam_reports.update": SuppressionSpamReportsUpdate,
"suppression.unsubscribes.create": SuppressionUnsubscribesCreate,
"suppression.unsubscribes.delete": SuppressionUnsubscribesDelete,
"suppression.unsubscribes.read": SuppressionUnsubscribesRead,
"suppression.unsubscribes.update": SuppressionUnsubscribesUpdate,
"suppression.update": SuppressionUpdate,
"teammates.create": TeammatesCreate,
"teammates.read": TeammatesRead,
"teammates.update": TeammatesUpdate,
"teammates.delete": TeammatesDelete,
"templates.create": TemplatesCreate,
"templates.delete": TemplatesDelete,
"templates.read": TemplatesRead,
"templates.update": TemplatesUpdate,
"templates.versions.activate.create": TemplatesVersionsActivateCreate,
"templates.versions.activate.delete": TemplatesVersionsActivateDelete,
"templates.versions.activate.read": TemplatesVersionsActivateRead,
"templates.versions.activate.update": TemplatesVersionsActivateUpdate,
"templates.versions.create": TemplatesVersionsCreate,
"templates.versions.delete": TemplatesVersionsDelete,
"templates.versions.read": TemplatesVersionsRead,
"templates.versions.update": TemplatesVersionsUpdate,
"tracking_settings.click.read": TrackingSettingsClickRead,
"tracking_settings.click.update": TrackingSettingsClickUpdate,
"tracking_settings.google_analytics.read": TrackingSettingsGoogleAnalyticsRead,
"tracking_settings.google_analytics.update": TrackingSettingsGoogleAnalyticsUpdate,
"tracking_settings.open.read": TrackingSettingsOpenRead,
"tracking_settings.open.update": TrackingSettingsOpenUpdate,
"tracking_settings.read": TrackingSettingsRead,
"tracking_settings.subscription.read": TrackingSettingsSubscriptionRead,
"tracking_settings.subscription.update": TrackingSettingsSubscriptionUpdate,
"user.account.read": UserAccountRead,
"user.credits.read": UserCreditsRead,
"user.email.create": UserEmailCreate,
"user.email.delete": UserEmailDelete,
"user.email.read": UserEmailRead,
"user.email.update": UserEmailUpdate,
"user.multifactor_authentication.create": UserMultifactorAuthenticationCreate,
"user.multifactor_authentication.delete": UserMultifactorAuthenticationDelete,
"user.multifactor_authentication.read": UserMultifactorAuthenticationRead,
"user.multifactor_authentication.update": UserMultifactorAuthenticationUpdate,
"user.password.read": UserPasswordRead,
"user.password.update": UserPasswordUpdate,
"user.profile.read": UserProfileRead,
"user.profile.update": UserProfileUpdate,
"user.scheduled_sends.create": UserScheduledSendsCreate,
"user.scheduled_sends.delete": UserScheduledSendsDelete,
"user.scheduled_sends.read": UserScheduledSendsRead,
"user.scheduled_sends.update": UserScheduledSendsUpdate,
"user.settings.enforced_tls.read": UserSettingsEnforcedTlsRead,
"user.settings.enforced_tls.update": UserSettingsEnforcedTlsUpdate,
"user.timezone.read": UserTimezoneRead,
"user.username.read": UserUsernameRead,
"user.username.update": UserUsernameUpdate,
"user.webhooks.event.settings.read": UserWebhooksEventSettingsRead,
"user.webhooks.event.settings.update": UserWebhooksEventSettingsUpdate,
"user.webhooks.event.test.create": UserWebhooksEventTestCreate,
"user.webhooks.event.test.read": UserWebhooksEventTestRead,
"user.webhooks.event.test.update": UserWebhooksEventTestUpdate,
"user.webhooks.parse.settings.create": UserWebhooksParseSettingsCreate,
"user.webhooks.parse.settings.delete": UserWebhooksParseSettingsDelete,
"user.webhooks.parse.settings.read": UserWebhooksParseSettingsRead,
"user.webhooks.parse.settings.update": UserWebhooksParseSettingsUpdate,
"user.webhooks.parse.stats.read": UserWebhooksParseStatsRead,
"whitelabel.create": WhitelabelCreate,
"whitelabel.delete": WhitelabelDelete,
"whitelabel.read": WhitelabelRead,
"whitelabel.update": WhitelabelUpdate,
}
PermissionIDs = map[Permission]int{
AccessSettingsActivityRead: 1,
AccessSettingsWhitelistCreate: 2,
AccessSettingsWhitelistDelete: 3,
AccessSettingsWhitelistRead: 4,
AccessSettingsWhitelistUpdate: 5,
AlertsCreate: 6,
AlertsDelete: 7,
AlertsRead: 8,
AlertsUpdate: 9,
ApiKeysCreate: 10,
ApiKeysDelete: 11,
ApiKeysRead: 12,
ApiKeysUpdate: 13,
AsmGroupsCreate: 14,
AsmGroupsDelete: 15,
AsmGroupsRead: 16,
AsmGroupsUpdate: 17,
BillingCreate: 18,
BillingDelete: 19,
BillingRead: 20,
BillingUpdate: 21,
BrowsersStatsRead: 22,
CategoriesCreate: 23,
CategoriesDelete: 24,
CategoriesRead: 25,
CategoriesStatsRead: 26,
CategoriesStatsSumsRead: 27,
CategoriesUpdate: 28,
ClientsDesktopStatsRead: 29,
ClientsPhoneStatsRead: 30,
ClientsStatsRead: 31,
ClientsTabletStatsRead: 32,
ClientsWebmailStatsRead: 33,
DevicesStatsRead: 34,
EmailActivityRead: 35,
GeoStatsRead: 36,
IpsAssignedRead: 37,
IpsPoolsCreate: 38,
IpsPoolsDelete: 39,
IpsPoolsIpsCreate: 40,
IpsPoolsIpsDelete: 41,
IpsPoolsIpsRead: 42,
IpsPoolsIpsUpdate: 43,
IpsPoolsRead: 44,
IpsPoolsUpdate: 45,
IpsRead: 46,
IpsWarmupCreate: 47,
IpsWarmupDelete: 48,
IpsWarmupRead: 49,
IpsWarmupUpdate: 50,
MailSettingsAddressWhitelistRead: 51,
MailSettingsAddressWhitelistUpdate: 52,
MailSettingsBouncePurgeRead: 53,
MailSettingsBouncePurgeUpdate: 54,
MailSettingsFooterRead: 55,
MailSettingsFooterUpdate: 56,
MailSettingsForwardBounceRead: 57,
MailSettingsForwardBounceUpdate: 58,
MailSettingsForwardSpamRead: 59,
MailSettingsForwardSpamUpdate: 60,
MailSettingsPlainContentRead: 61,
MailSettingsPlainContentUpdate: 62,
MailSettingsRead: 63,
MailSettingsTemplateRead: 64,
MailSettingsTemplateUpdate: 65,
MailBatchCreate: 66,
MailBatchDelete: 67,
MailBatchRead: 68,
MailBatchUpdate: 69,
MailSend: 70,
MailboxProvidersStatsRead: 71,
MarketingCampaignsCreate: 72,
MarketingCampaignsDelete: 73,
MarketingCampaignsRead: 74,
MarketingCampaignsUpdate: 75,
PartnerSettingsNewRelicRead: 76,
PartnerSettingsNewRelicUpdate: 77,
PartnerSettingsRead: 78,
StatsGlobalRead: 79,
StatsRead: 80,
SubusersCreate: 81,
SubusersCreditsCreate: 82,
SubusersCreditsDelete: 83,
SubusersCreditsRead: 84,
SubusersCreditsRemainingCreate: 85,
SubusersCreditsRemainingDelete: 86,
SubusersCreditsRemainingRead: 87,
SubusersCreditsRemainingUpdate: 88,
SubusersCreditsUpdate: 89,
SubusersDelete: 90,
SubusersMonitorCreate: 91,
SubusersMonitorDelete: 92,
SubusersMonitorRead: 93,
SubusersMonitorUpdate: 94,
SubusersRead: 95,
SubusersReputationsRead: 96,
SubusersStatsMonthlyRead: 97,
SubusersStatsRead: 98,
SubusersStatsSumsRead: 99,
SubusersSummaryRead: 100,
SubusersUpdate: 101,
SuppressionBlocksCreate: 102,
SuppressionBlocksDelete: 103,
SuppressionBlocksRead: 104,
SuppressionBlocksUpdate: 105,
SuppressionBouncesCreate: 106,
SuppressionBouncesDelete: 107,
SuppressionBouncesRead: 108,
SuppressionBouncesUpdate: 109,
SuppressionCreate: 110,
SuppressionDelete: 111,
SuppressionInvalidEmailsCreate: 112,
SuppressionInvalidEmailsDelete: 113,
SuppressionInvalidEmailsRead: 114,
SuppressionInvalidEmailsUpdate: 115,
SuppressionRead: 116,
SuppressionSpamReportsCreate: 117,
SuppressionSpamReportsDelete: 118,
SuppressionSpamReportsRead: 119,
SuppressionSpamReportsUpdate: 120,
SuppressionUnsubscribesCreate: 121,
SuppressionUnsubscribesDelete: 122,
SuppressionUnsubscribesRead: 123,
SuppressionUnsubscribesUpdate: 124,
SuppressionUpdate: 125,
TeammatesCreate: 126,
TeammatesRead: 127,
TeammatesUpdate: 128,
TeammatesDelete: 129,
TemplatesCreate: 130,
TemplatesDelete: 131,
TemplatesRead: 132,
TemplatesUpdate: 133,
TemplatesVersionsActivateCreate: 134,
TemplatesVersionsActivateDelete: 135,
TemplatesVersionsActivateRead: 136,
TemplatesVersionsActivateUpdate: 137,
TemplatesVersionsCreate: 138,
TemplatesVersionsDelete: 139,
TemplatesVersionsRead: 140,
TemplatesVersionsUpdate: 141,
TrackingSettingsClickRead: 142,
TrackingSettingsClickUpdate: 143,
TrackingSettingsGoogleAnalyticsRead: 144,
TrackingSettingsGoogleAnalyticsUpdate: 145,
TrackingSettingsOpenRead: 146,
TrackingSettingsOpenUpdate: 147,
TrackingSettingsRead: 148,
TrackingSettingsSubscriptionRead: 149,
TrackingSettingsSubscriptionUpdate: 150,
UserAccountRead: 151,
UserCreditsRead: 152,
UserEmailCreate: 153,
UserEmailDelete: 154,
UserEmailRead: 155,
UserEmailUpdate: 156,
UserMultifactorAuthenticationCreate: 157,
UserMultifactorAuthenticationDelete: 158,
UserMultifactorAuthenticationRead: 159,
UserMultifactorAuthenticationUpdate: 160,
UserPasswordRead: 161,
UserPasswordUpdate: 162,
UserProfileRead: 163,
UserProfileUpdate: 164,
UserScheduledSendsCreate: 165,
UserScheduledSendsDelete: 166,
UserScheduledSendsRead: 167,
UserScheduledSendsUpdate: 168,
UserSettingsEnforcedTlsRead: 169,
UserSettingsEnforcedTlsUpdate: 170,
UserTimezoneRead: 171,
UserUsernameRead: 172,
UserUsernameUpdate: 173,
UserWebhooksEventSettingsRead: 174,
UserWebhooksEventSettingsUpdate: 175,
UserWebhooksEventTestCreate: 176,
UserWebhooksEventTestRead: 177,
UserWebhooksEventTestUpdate: 178,
UserWebhooksParseSettingsCreate: 179,
UserWebhooksParseSettingsDelete: 180,
UserWebhooksParseSettingsRead: 181,
UserWebhooksParseSettingsUpdate: 182,
UserWebhooksParseStatsRead: 183,
WhitelabelCreate: 184,
WhitelabelDelete: 185,
WhitelabelRead: 186,
WhitelabelUpdate: 187,
}
IdToPermission = map[int]Permission{
1: AccessSettingsActivityRead,
2: AccessSettingsWhitelistCreate,
3: AccessSettingsWhitelistDelete,
4: AccessSettingsWhitelistRead,
5: AccessSettingsWhitelistUpdate,
6: AlertsCreate,
7: AlertsDelete,
8: AlertsRead,
9: AlertsUpdate,
10: ApiKeysCreate,
11: ApiKeysDelete,
12: ApiKeysRead,
13: ApiKeysUpdate,
14: AsmGroupsCreate,
15: AsmGroupsDelete,
16: AsmGroupsRead,
17: AsmGroupsUpdate,
18: BillingCreate,
19: BillingDelete,
20: BillingRead,
21: BillingUpdate,
22: BrowsersStatsRead,
23: CategoriesCreate,
24: CategoriesDelete,
25: CategoriesRead,
26: CategoriesStatsRead,
27: CategoriesStatsSumsRead,
28: CategoriesUpdate,
29: ClientsDesktopStatsRead,
30: ClientsPhoneStatsRead,
31: ClientsStatsRead,
32: ClientsTabletStatsRead,
33: ClientsWebmailStatsRead,
34: DevicesStatsRead,
35: EmailActivityRead,
36: GeoStatsRead,
37: IpsAssignedRead,
38: IpsPoolsCreate,
39: IpsPoolsDelete,
40: IpsPoolsIpsCreate,
41: IpsPoolsIpsDelete,
42: IpsPoolsIpsRead,
43: IpsPoolsIpsUpdate,
44: IpsPoolsRead,
45: IpsPoolsUpdate,
46: IpsRead,
47: IpsWarmupCreate,
48: IpsWarmupDelete,
49: IpsWarmupRead,
50: IpsWarmupUpdate,
51: MailSettingsAddressWhitelistRead,
52: MailSettingsAddressWhitelistUpdate,
53: MailSettingsBouncePurgeRead,
54: MailSettingsBouncePurgeUpdate,
55: MailSettingsFooterRead,
56: MailSettingsFooterUpdate,
57: MailSettingsForwardBounceRead,
58: MailSettingsForwardBounceUpdate,
59: MailSettingsForwardSpamRead,
60: MailSettingsForwardSpamUpdate,
61: MailSettingsPlainContentRead,
62: MailSettingsPlainContentUpdate,
63: MailSettingsRead,
64: MailSettingsTemplateRead,
65: MailSettingsTemplateUpdate,
66: MailBatchCreate,
67: MailBatchDelete,
68: MailBatchRead,
69: MailBatchUpdate,
70: MailSend,
71: MailboxProvidersStatsRead,
72: MarketingCampaignsCreate,
73: MarketingCampaignsDelete,
74: MarketingCampaignsRead,
75: MarketingCampaignsUpdate,
76: PartnerSettingsNewRelicRead,
77: PartnerSettingsNewRelicUpdate,
78: PartnerSettingsRead,
79: StatsGlobalRead,
80: StatsRead,
81: SubusersCreate,
82: SubusersCreditsCreate,
83: SubusersCreditsDelete,
84: SubusersCreditsRead,
85: SubusersCreditsRemainingCreate,
86: SubusersCreditsRemainingDelete,
87: SubusersCreditsRemainingRead,
88: SubusersCreditsRemainingUpdate,
89: SubusersCreditsUpdate,
90: SubusersDelete,
91: SubusersMonitorCreate,
92: SubusersMonitorDelete,
93: SubusersMonitorRead,
94: SubusersMonitorUpdate,
95: SubusersRead,
96: SubusersReputationsRead,
97: SubusersStatsMonthlyRead,
98: SubusersStatsRead,
99: SubusersStatsSumsRead,
100: SubusersSummaryRead,
101: SubusersUpdate,
102: SuppressionBlocksCreate,
103: SuppressionBlocksDelete,
104: SuppressionBlocksRead,
105: SuppressionBlocksUpdate,
106: SuppressionBouncesCreate,
107: SuppressionBouncesDelete,
108: SuppressionBouncesRead,
109: SuppressionBouncesUpdate,
110: SuppressionCreate,
111: SuppressionDelete,
112: SuppressionInvalidEmailsCreate,
113: SuppressionInvalidEmailsDelete,
114: SuppressionInvalidEmailsRead,
115: SuppressionInvalidEmailsUpdate,
116: SuppressionRead,
117: SuppressionSpamReportsCreate,
118: SuppressionSpamReportsDelete,
119: SuppressionSpamReportsRead,
120: SuppressionSpamReportsUpdate,
121: SuppressionUnsubscribesCreate,
122: SuppressionUnsubscribesDelete,
123: SuppressionUnsubscribesRead,
124: SuppressionUnsubscribesUpdate,
125: SuppressionUpdate,
126: TeammatesCreate,
127: TeammatesRead,
128: TeammatesUpdate,
129: TeammatesDelete,
130: TemplatesCreate,
131: TemplatesDelete,
132: TemplatesRead,
133: TemplatesUpdate,
134: TemplatesVersionsActivateCreate,
135: TemplatesVersionsActivateDelete,
136: TemplatesVersionsActivateRead,
137: TemplatesVersionsActivateUpdate,
138: TemplatesVersionsCreate,
139: TemplatesVersionsDelete,
140: TemplatesVersionsRead,
141: TemplatesVersionsUpdate,
142: TrackingSettingsClickRead,
143: TrackingSettingsClickUpdate,
144: TrackingSettingsGoogleAnalyticsRead,
145: TrackingSettingsGoogleAnalyticsUpdate,
146: TrackingSettingsOpenRead,
147: TrackingSettingsOpenUpdate,
148: TrackingSettingsRead,
149: TrackingSettingsSubscriptionRead,
150: TrackingSettingsSubscriptionUpdate,
151: UserAccountRead,
152: UserCreditsRead,
153: UserEmailCreate,
154: UserEmailDelete,
155: UserEmailRead,
156: UserEmailUpdate,
157: UserMultifactorAuthenticationCreate,
158: UserMultifactorAuthenticationDelete,
159: UserMultifactorAuthenticationRead,
160: UserMultifactorAuthenticationUpdate,
161: UserPasswordRead,
162: UserPasswordUpdate,
163: UserProfileRead,
164: UserProfileUpdate,
165: UserScheduledSendsCreate,
166: UserScheduledSendsDelete,
167: UserScheduledSendsRead,
168: UserScheduledSendsUpdate,
169: UserSettingsEnforcedTlsRead,
170: UserSettingsEnforcedTlsUpdate,
171: UserTimezoneRead,
172: UserUsernameRead,
173: UserUsernameUpdate,
174: UserWebhooksEventSettingsRead,
175: UserWebhooksEventSettingsUpdate,
176: UserWebhooksEventTestCreate,
177: UserWebhooksEventTestRead,
178: UserWebhooksEventTestUpdate,
179: UserWebhooksParseSettingsCreate,
180: UserWebhooksParseSettingsDelete,
181: UserWebhooksParseSettingsRead,
182: UserWebhooksParseSettingsUpdate,
183: UserWebhooksParseStatsRead,
184: WhitelabelCreate,
185: WhitelabelDelete,
186: WhitelabelRead,
187: WhitelabelUpdate,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/sendgrid/permissions.yaml
================================================
permissions:
- access_settings.activity.read
- access_settings.whitelist.create
- access_settings.whitelist.delete
- access_settings.whitelist.read
- access_settings.whitelist.update
- alerts.create
- alerts.delete
- alerts.read
- alerts.update
- api_keys.create
- api_keys.delete
- api_keys.read
- api_keys.update
- asm.groups.create
- asm.groups.delete
- asm.groups.read
- asm.groups.update
- billing.create
- billing.delete
- billing.read
- billing.update
- browsers.stats.read
- categories.create
- categories.delete
- categories.read
- categories.stats.read
- categories.stats.sums.read
- categories.update
- clients.desktop.stats.read
- clients.phone.stats.read
- clients.stats.read
- clients.tablet.stats.read
- clients.webmail.stats.read
- devices.stats.read
- email_activity.read
- geo.stats.read
- ips.assigned.read
- ips.pools.create
- ips.pools.delete
- ips.pools.ips.create
- ips.pools.ips.delete
- ips.pools.ips.read
- ips.pools.ips.update
- ips.pools.read
- ips.pools.update
- ips.read
- ips.warmup.create
- ips.warmup.delete
- ips.warmup.read
- ips.warmup.update
- mail_settings.address_whitelist.read
- mail_settings.address_whitelist.update
- mail_settings.bounce_purge.read
- mail_settings.bounce_purge.update
- mail_settings.footer.read
- mail_settings.footer.update
- mail_settings.forward_bounce.read
- mail_settings.forward_bounce.update
- mail_settings.forward_spam.read
- mail_settings.forward_spam.update
- mail_settings.plain_content.read
- mail_settings.plain_content.update
- mail_settings.read
- mail_settings.template.read
- mail_settings.template.update
- mail.batch.create
- mail.batch.delete
- mail.batch.read
- mail.batch.update
- mail.send
- mailbox_providers.stats.read
- marketing_campaigns.create
- marketing_campaigns.delete
- marketing_campaigns.read
- marketing_campaigns.update
- partner_settings.new_relic.read
- partner_settings.new_relic.update
- partner_settings.read
- stats.global.read
- stats.read
- subusers.create
- subusers.credits.create
- subusers.credits.delete
- subusers.credits.read
- subusers.credits.remaining.create
- subusers.credits.remaining.delete
- subusers.credits.remaining.read
- subusers.credits.remaining.update
- subusers.credits.update
- subusers.delete
- subusers.monitor.create
- subusers.monitor.delete
- subusers.monitor.read
- subusers.monitor.update
- subusers.read
- subusers.reputations.read
- subusers.stats.monthly.read
- subusers.stats.read
- subusers.stats.sums.read
- subusers.summary.read
- subusers.update
- suppression.blocks.create
- suppression.blocks.delete
- suppression.blocks.read
- suppression.blocks.update
- suppression.bounces.create
- suppression.bounces.delete
- suppression.bounces.read
- suppression.bounces.update
- suppression.create
- suppression.delete
- suppression.invalid_emails.create
- suppression.invalid_emails.delete
- suppression.invalid_emails.read
- suppression.invalid_emails.update
- suppression.read
- suppression.spam_reports.create
- suppression.spam_reports.delete
- suppression.spam_reports.read
- suppression.spam_reports.update
- suppression.unsubscribes.create
- suppression.unsubscribes.delete
- suppression.unsubscribes.read
- suppression.unsubscribes.update
- suppression.update
- teammates.create
- teammates.read
- teammates.update
- teammates.delete
- templates.create
- templates.delete
- templates.read
- templates.update
- templates.versions.activate.create
- templates.versions.activate.delete
- templates.versions.activate.read
- templates.versions.activate.update
- templates.versions.create
- templates.versions.delete
- templates.versions.read
- templates.versions.update
- tracking_settings.click.read
- tracking_settings.click.update
- tracking_settings.google_analytics.read
- tracking_settings.google_analytics.update
- tracking_settings.open.read
- tracking_settings.open.update
- tracking_settings.read
- tracking_settings.subscription.read
- tracking_settings.subscription.update
- user.account.read
- user.credits.read
- user.email.create
- user.email.delete
- user.email.read
- user.email.update
- user.multifactor_authentication.create
- user.multifactor_authentication.delete
- user.multifactor_authentication.read
- user.multifactor_authentication.update
- user.password.read
- user.password.update
- user.profile.read
- user.profile.update
- user.scheduled_sends.create
- user.scheduled_sends.delete
- user.scheduled_sends.read
- user.scheduled_sends.update
- user.settings.enforced_tls.read
- user.settings.enforced_tls.update
- user.timezone.read
- user.username.read
- user.username.update
- user.webhooks.event.settings.read
- user.webhooks.event.settings.update
- user.webhooks.event.test.create
- user.webhooks.event.test.read
- user.webhooks.event.test.update
- user.webhooks.parse.settings.create
- user.webhooks.parse.settings.delete
- user.webhooks.parse.settings.read
- user.webhooks.parse.settings.update
- user.webhooks.parse.stats.read
- whitelabel.create
- whitelabel.delete
- whitelabel.read
- whitelabel.update
================================================
FILE: pkg/analyzer/analyzers/sendgrid/result_output.json
================================================
{
"AnalyzerType": 16,
"Bindings": [
{
"Resource": {
"Name": "API Keys",
"FullyQualifiedName": "API Keys",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "api_keys.create",
"Parent": null
}
},
{
"Resource": {
"Name": "API Keys",
"FullyQualifiedName": "API Keys",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "api_keys.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "API Keys",
"FullyQualifiedName": "API Keys",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "api_keys.read",
"Parent": null
}
},
{
"Resource": {
"Name": "API Keys",
"FullyQualifiedName": "API Keys",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "api_keys.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Account",
"FullyQualifiedName": "User Account/Account",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.account.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Address Allow List",
"FullyQualifiedName": "Mail Settings/Address Allow List",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mail_settings.address_whitelist.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Address Allow List",
"FullyQualifiedName": "Mail Settings/Address Allow List",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mail_settings.address_whitelist.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Alerts",
"FullyQualifiedName": "Alerts",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "alerts.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Alerts",
"FullyQualifiedName": "Alerts",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "alerts.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Alerts",
"FullyQualifiedName": "Alerts",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "alerts.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Alerts",
"FullyQualifiedName": "Alerts",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "alerts.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Bounce Purge",
"FullyQualifiedName": "Mail Settings/Bounce Purge",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mail_settings.bounce_purge.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Bounce Purge",
"FullyQualifiedName": "Mail Settings/Bounce Purge",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mail_settings.bounce_purge.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Browser Stats",
"FullyQualifiedName": "Stats/Browser Stats",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "browsers.stats.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Category Management",
"FullyQualifiedName": "Category Management",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "categories.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Category Management",
"FullyQualifiedName": "Category Management",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "categories.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Category Management",
"FullyQualifiedName": "Category Management",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "categories.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Category Management",
"FullyQualifiedName": "Category Management",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "categories.stats.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Category Management",
"FullyQualifiedName": "Category Management",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "categories.stats.sums.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Category Management",
"FullyQualifiedName": "Category Management",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "categories.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Click Tracking",
"FullyQualifiedName": "Tracking/Click Tracking",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Tracking",
"FullyQualifiedName": "Tracking",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "tracking_settings.click.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Click Tracking",
"FullyQualifiedName": "Tracking/Click Tracking",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Tracking",
"FullyQualifiedName": "Tracking",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "tracking_settings.click.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Credits",
"FullyQualifiedName": "User Account/Credits",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.credits.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Email",
"FullyQualifiedName": "User Account/Email",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.email.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Email",
"FullyQualifiedName": "User Account/Email",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.email.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Email",
"FullyQualifiedName": "User Account/Email",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.email.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Email",
"FullyQualifiedName": "User Account/Email",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.email.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Email Clients and Devices",
"FullyQualifiedName": "Stats/Email Clients and Devices",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "clients.desktop.stats.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Email Clients and Devices",
"FullyQualifiedName": "Stats/Email Clients and Devices",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "clients.phone.stats.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Email Clients and Devices",
"FullyQualifiedName": "Stats/Email Clients and Devices",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "clients.stats.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Email Clients and Devices",
"FullyQualifiedName": "Stats/Email Clients and Devices",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "clients.tablet.stats.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Email Clients and Devices",
"FullyQualifiedName": "Stats/Email Clients and Devices",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "clients.webmail.stats.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Email Clients and Devices",
"FullyQualifiedName": "Stats/Email Clients and Devices",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "devices.stats.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Enforced TLS",
"FullyQualifiedName": "User Account/Enforced TLS",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.settings.enforced_tls.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Enforced TLS",
"FullyQualifiedName": "User Account/Enforced TLS",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.settings.enforced_tls.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Event Notification",
"FullyQualifiedName": "Mail Settings/Event Notification",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.webhooks.event.settings.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Event Notification",
"FullyQualifiedName": "Mail Settings/Event Notification",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.webhooks.event.settings.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Event Notification",
"FullyQualifiedName": "Mail Settings/Event Notification",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.webhooks.event.test.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Event Notification",
"FullyQualifiedName": "Mail Settings/Event Notification",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.webhooks.event.test.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Event Notification",
"FullyQualifiedName": "Mail Settings/Event Notification",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.webhooks.event.test.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Footer",
"FullyQualifiedName": "Mail Settings/Footer",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mail_settings.footer.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Footer",
"FullyQualifiedName": "Mail Settings/Footer",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mail_settings.footer.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Forward Bounce",
"FullyQualifiedName": "Mail Settings/Forward Bounce",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mail_settings.forward_bounce.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Forward Bounce",
"FullyQualifiedName": "Mail Settings/Forward Bounce",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mail_settings.forward_bounce.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Forward Spam",
"FullyQualifiedName": "Mail Settings/Forward Spam",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mail_settings.forward_spam.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Forward Spam",
"FullyQualifiedName": "Mail Settings/Forward Spam",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mail_settings.forward_spam.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Geographical",
"FullyQualifiedName": "Stats/Geographical",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "geo.stats.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Global Stats",
"FullyQualifiedName": "Stats/Global Stats",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "stats.global.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Google Analytics",
"FullyQualifiedName": "Tracking/Google Analytics",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Tracking",
"FullyQualifiedName": "Tracking",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "tracking_settings.google_analytics.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Google Analytics",
"FullyQualifiedName": "Tracking/Google Analytics",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Tracking",
"FullyQualifiedName": "Tracking",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "tracking_settings.google_analytics.update",
"Parent": null
}
},
{
"Resource": {
"Name": "IP Management",
"FullyQualifiedName": "IP Management",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "ips.pools.ips.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Inbound Parse",
"FullyQualifiedName": "Inbound Parse",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "user.webhooks.parse.settings.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Inbound Parse",
"FullyQualifiedName": "Inbound Parse",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "user.webhooks.parse.settings.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Inbound Parse",
"FullyQualifiedName": "Inbound Parse",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "user.webhooks.parse.settings.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Inbound Parse",
"FullyQualifiedName": "Inbound Parse",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "user.webhooks.parse.settings.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Legacy Email Template",
"FullyQualifiedName": "Mail Settings/Legacy Email Template",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mail_settings.template.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Legacy Email Template",
"FullyQualifiedName": "Mail Settings/Legacy Email Template",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mail_settings.template.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Mail Send",
"FullyQualifiedName": "Mail Send/Mail Send",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Send",
"FullyQualifiedName": "Mail Send",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mail.send",
"Parent": null
}
},
{
"Resource": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "mail_settings.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Mailbox Provider Stats",
"FullyQualifiedName": "Stats/Mailbox Provider Stats",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mailbox_providers.stats.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Multifactor Authentication",
"FullyQualifiedName": "User Account/Multifactor Authentication",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.multifactor_authentication.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Multifactor Authentication",
"FullyQualifiedName": "User Account/Multifactor Authentication",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.multifactor_authentication.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Multifactor Authentication",
"FullyQualifiedName": "User Account/Multifactor Authentication",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.multifactor_authentication.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Multifactor Authentication",
"FullyQualifiedName": "User Account/Multifactor Authentication",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.multifactor_authentication.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Open Tracking",
"FullyQualifiedName": "Tracking/Open Tracking",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Tracking",
"FullyQualifiedName": "Tracking",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "tracking_settings.open.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Open Tracking",
"FullyQualifiedName": "Tracking/Open Tracking",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Tracking",
"FullyQualifiedName": "Tracking",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "tracking_settings.open.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Parse Webhook",
"FullyQualifiedName": "Stats/Parse Webhook",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.webhooks.parse.stats.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Partners",
"FullyQualifiedName": "Partners",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "partner_settings.new_relic.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Partners",
"FullyQualifiedName": "Partners",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "partner_settings.new_relic.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Partners",
"FullyQualifiedName": "Partners",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "partner_settings.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Password",
"FullyQualifiedName": "User Account/Password",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.password.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Password",
"FullyQualifiedName": "User Account/Password",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.password.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Plain Content",
"FullyQualifiedName": "Mail Settings/Plain Content",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mail_settings.plain_content.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Plain Content",
"FullyQualifiedName": "Mail Settings/Plain Content",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "mail_settings.plain_content.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Profile",
"FullyQualifiedName": "User Account/Profile",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.profile.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Profile",
"FullyQualifiedName": "User Account/Profile",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.profile.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Security",
"FullyQualifiedName": "Security",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "access_settings.activity.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Security",
"FullyQualifiedName": "Security",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "access_settings.whitelist.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Security",
"FullyQualifiedName": "Security",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "access_settings.whitelist.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Security",
"FullyQualifiedName": "Security",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "access_settings.whitelist.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Security",
"FullyQualifiedName": "Security",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "access_settings.whitelist.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Sender Authentication",
"FullyQualifiedName": "Sender Authentication",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "whitelabel.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Sender Authentication",
"FullyQualifiedName": "Sender Authentication",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "whitelabel.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Sender Authentication",
"FullyQualifiedName": "Sender Authentication",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "whitelabel.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Sender Authentication",
"FullyQualifiedName": "Sender Authentication",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "whitelabel.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Source Integration",
"FullyQualifiedName": "37006899",
"Type": "User",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Stats Overview",
"FullyQualifiedName": "Stats/Stats Overview",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "stats.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Subscription Tracking",
"FullyQualifiedName": "Tracking/Subscription Tracking",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Tracking",
"FullyQualifiedName": "Tracking",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "tracking_settings.subscription.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Subscription Tracking",
"FullyQualifiedName": "Tracking/Subscription Tracking",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Tracking",
"FullyQualifiedName": "Tracking",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "tracking_settings.subscription.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Subuser Stats",
"FullyQualifiedName": "Stats/Subuser Stats",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "subusers.stats.monthly.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Subuser Stats",
"FullyQualifiedName": "Stats/Subuser Stats",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "subusers.stats.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Subuser Stats",
"FullyQualifiedName": "Stats/Subuser Stats",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "subusers.stats.sums.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.blocks.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.blocks.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.blocks.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.blocks.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.bounces.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.bounces.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.bounces.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.bounces.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.invalid_emails.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.invalid_emails.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.invalid_emails.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.invalid_emails.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.spam_reports.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.spam_reports.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.spam_reports.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.spam_reports.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.unsubscribes.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.unsubscribes.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.unsubscribes.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.unsubscribes.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Supressions",
"FullyQualifiedName": "Suppressions/Supressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "suppression.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Teammates",
"FullyQualifiedName": "Teammates",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "teammates.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Teammates",
"FullyQualifiedName": "Teammates",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "teammates.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Teammates",
"FullyQualifiedName": "Teammates",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "teammates.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Teammates",
"FullyQualifiedName": "Teammates",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "teammates.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Template Engine",
"FullyQualifiedName": "Template Engine",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "templates.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Template Engine",
"FullyQualifiedName": "Template Engine",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "templates.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Template Engine",
"FullyQualifiedName": "Template Engine",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "templates.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Template Engine",
"FullyQualifiedName": "Template Engine",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "templates.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Template Engine",
"FullyQualifiedName": "Template Engine",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "templates.versions.activate.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Template Engine",
"FullyQualifiedName": "Template Engine",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "templates.versions.activate.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Template Engine",
"FullyQualifiedName": "Template Engine",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "templates.versions.activate.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Template Engine",
"FullyQualifiedName": "Template Engine",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "templates.versions.activate.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Template Engine",
"FullyQualifiedName": "Template Engine",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "templates.versions.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Template Engine",
"FullyQualifiedName": "Template Engine",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "templates.versions.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Template Engine",
"FullyQualifiedName": "Template Engine",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "templates.versions.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Template Engine",
"FullyQualifiedName": "Template Engine",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "templates.versions.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Timezone",
"FullyQualifiedName": "User Account/Timezone",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.timezone.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Tracking",
"FullyQualifiedName": "Tracking",
"Type": "category",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "tracking_settings.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Unsubscribe Groups",
"FullyQualifiedName": "Suppressions/Unsubscribe Groups",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "asm.groups.create",
"Parent": null
}
},
{
"Resource": {
"Name": "Unsubscribe Groups",
"FullyQualifiedName": "Suppressions/Unsubscribe Groups",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "asm.groups.delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Unsubscribe Groups",
"FullyQualifiedName": "Suppressions/Unsubscribe Groups",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "asm.groups.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Unsubscribe Groups",
"FullyQualifiedName": "Suppressions/Unsubscribe Groups",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "asm.groups.update",
"Parent": null
}
},
{
"Resource": {
"Name": "Username",
"FullyQualifiedName": "User Account/Username",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.username.read",
"Parent": null
}
},
{
"Resource": {
"Name": "Username",
"FullyQualifiedName": "User Account/Username",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "User Account",
"FullyQualifiedName": "User Account",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "user.username.update",
"Parent": null
}
}
],
"UnboundedResources": [
{
"Name": "Billing",
"FullyQualifiedName": "Billing",
"Type": "category",
"Metadata": null,
"Parent": null
},
{
"Name": "Design Library",
"FullyQualifiedName": "Design Library",
"Type": "category",
"Metadata": null,
"Parent": null
},
{
"Name": "Email Activity",
"FullyQualifiedName": "Email Activity",
"Type": "category",
"Metadata": null,
"Parent": null
},
{
"Name": "Email Testing",
"FullyQualifiedName": "Email Testing",
"Type": "category",
"Metadata": null,
"Parent": null
},
{
"Name": "Scheduled Sends",
"FullyQualifiedName": "Mail Send/Scheduled Sends",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Send",
"FullyQualifiedName": "Mail Send",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
{
"Name": "BCC",
"FullyQualifiedName": "Mail Settings/BCC",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
{
"Name": "Spam Checker",
"FullyQualifiedName": "Mail Settings/Spam Checker",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Mail Settings",
"FullyQualifiedName": "Mail Settings",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
{
"Name": "Automation",
"FullyQualifiedName": "Marketing/Automation",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Marketing",
"FullyQualifiedName": "Marketing",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
{
"Name": "Marketing",
"FullyQualifiedName": "Marketing/Marketing",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Marketing",
"FullyQualifiedName": "Marketing",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
{
"Name": "Recipients Data Erasure",
"FullyQualifiedName": "Recipients Data Erasure",
"Type": "category",
"Metadata": null,
"Parent": null
},
{
"Name": "Category Stats",
"FullyQualifiedName": "Stats/Category Stats",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Stats",
"FullyQualifiedName": "Stats",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
{
"Name": "Unsubscribe Group Suppressions",
"FullyQualifiedName": "Suppressions/Unsubscribe Group Suppressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
{
"Name": "Global Suppressions",
"FullyQualifiedName": "Suppressions/Global Suppressions",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
{
"Name": "Credentials",
"FullyQualifiedName": "Credentials",
"Type": "category",
"Metadata": null,
"Parent": null
},
{
"Name": "Signup",
"FullyQualifiedName": "Signup",
"Type": "category",
"Metadata": null,
"Parent": null
},
{
"Name": "Blocks",
"FullyQualifiedName": "Suppressions/Blocks",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
{
"Name": "Bounces",
"FullyQualifiedName": "Suppressions/Bounces",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
{
"Name": "Invalid Emails",
"FullyQualifiedName": "Suppressions/Invalid Emails",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
{
"Name": "Spam Reports",
"FullyQualifiedName": "Suppressions/Spam Reports",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
{
"Name": "Unsubscribes",
"FullyQualifiedName": "Suppressions/Unsubscribes",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "Suppressions",
"FullyQualifiedName": "Suppressions",
"Type": "category",
"Metadata": null,
"Parent": null
}
},
{
"Name": "UI",
"FullyQualifiedName": "UI",
"Type": "category",
"Metadata": null,
"Parent": null
}
],
"Metadata": {
"2fa_required": true,
"key_type": "full access"
}
}
================================================
FILE: pkg/analyzer/analyzers/sendgrid/scopes.go
================================================
package sendgrid
import (
"strings"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
)
type SendgridScope struct {
Category string
SubCategory string
Prefixes []string // Prefixes for the scope
Permissions []string
PermissionType analyzers.PermissionType
}
func (s *SendgridScope) AddPermission(permission string) {
s.Permissions = append(s.Permissions, permission)
}
func (s *SendgridScope) RunTests() {
if len(s.Permissions) == 0 {
s.PermissionType = analyzers.NONE
return
}
for _, permission := range s.Permissions {
if strings.Contains(permission, ".read") {
s.PermissionType = analyzers.READ
} else {
s.PermissionType = analyzers.READ_WRITE
return
}
}
}
var SCOPES = []SendgridScope{
// Billing
{Category: "Billing", Prefixes: []string{"billing"}},
// Restricted Access
{Category: "API Keys", Prefixes: []string{"api_keys"}},
{Category: "Alerts", Prefixes: []string{"alerts"}},
{Category: "Category Management", Prefixes: []string{"categories"}},
{Category: "Design Library", Prefixes: []string{"design_library"}},
{Category: "Email Activity", Prefixes: []string{"messages"}},
{Category: "Email Testing", Prefixes: []string{"email_testing"}},
{Category: "IP Management", Prefixes: []string{"ips"}},
{Category: "Inbound Parse", Prefixes: []string{"user.webhooks.parse.settings"}},
{Category: "Mail Send", SubCategory: "Mail Send", Prefixes: []string{"mail.send"}},
{Category: "Mail Send", SubCategory: "Scheduled Sends", Prefixes: []string{"user.scheduled_sends, mail.batch"}},
{Category: "Mail Settings", SubCategory: "Address Allow List", Prefixes: []string{"mail_settings.address_whitelist"}},
{Category: "Mail Settings", SubCategory: "BCC", Prefixes: []string{"mail_settings.bcc"}},
{Category: "Mail Settings", SubCategory: "Bounce Purge", Prefixes: []string{"mail_settings.bounce_purge"}},
{Category: "Mail Settings", SubCategory: "Event Notification", Prefixes: []string{"user.webhooks.event"}},
{Category: "Mail Settings", SubCategory: "Footer", Prefixes: []string{"mail_settings.footer"}},
{Category: "Mail Settings", SubCategory: "Forward Bounce", Prefixes: []string{"mail_settings.forward_bounce"}},
{Category: "Mail Settings", SubCategory: "Forward Spam", Prefixes: []string{"mail_settings.forward_spam"}},
{Category: "Mail Settings", SubCategory: "Legacy Email Template", Prefixes: []string{"mail_settings.template"}},
{Category: "Mail Settings", SubCategory: "Plain Content", Prefixes: []string{"mail_settings.plain_content"}},
{Category: "Mail Settings", SubCategory: "Spam Checker", Prefixes: []string{"mail_settings.spam_check"}},
{Category: "Marketing", SubCategory: "Automation", Prefixes: []string{"marketing.automation"}},
{Category: "Marketing", SubCategory: "Marketing", Prefixes: []string{"marketing.read"}},
{Category: "Partners", Prefixes: []string{"partner_settings"}},
{Category: "Recipients Data Erasure", Prefixes: []string{"recipients"}},
{Category: "Security", Prefixes: []string{"access_settings"}},
{Category: "Sender Authentication", Prefixes: []string{"whitelabel"}},
{Category: "Stats", SubCategory: "Browser Stats", Prefixes: []string{"browsers"}},
{Category: "Stats", SubCategory: "Category Stats", Prefixes: []string{"categories.stats"}},
{Category: "Stats", SubCategory: "Email Clients and Devices", Prefixes: []string{"clients", "devices"}},
{Category: "Stats", SubCategory: "Geographical", Prefixes: []string{"geo"}},
{Category: "Stats", SubCategory: "Global Stats", Prefixes: []string{"stats.global"}},
{Category: "Stats", SubCategory: "Mailbox Provider Stats", Prefixes: []string{"mailbox_providers"}},
{Category: "Stats", SubCategory: "Parse Webhook", Prefixes: []string{"user.webhooks.parse.stats"}},
{Category: "Stats", SubCategory: "Stats Overview", Prefixes: []string{"stats.read"}},
{Category: "Stats", SubCategory: "Subuser Stats", Prefixes: []string{"subusers"}},
{Category: "Suppressions", SubCategory: "Supressions", Prefixes: []string{"suppression"}},
{Category: "Suppressions", SubCategory: "Unsubscribe Groups", Prefixes: []string{"asm.groups"}},
{Category: "Template Engine", Prefixes: []string{"templates"}},
{Category: "Tracking", SubCategory: "Click Tracking", Prefixes: []string{"tracking_settings.click"}},
{Category: "Tracking", SubCategory: "Google Analytics", Prefixes: []string{"tracking_settings.google_analytics"}},
{Category: "Tracking", SubCategory: "Open Tracking", Prefixes: []string{"tracking_settings.open"}},
{Category: "Tracking", SubCategory: "Subscription Tracking", Prefixes: []string{"tracking_settings.subscription"}},
{Category: "User Account", SubCategory: "Enforced TLS", Prefixes: []string{"user.settings.enforced_tls"}},
{Category: "User Account", SubCategory: "Timezone", Prefixes: []string{"user.timezone"}},
// Full Access Additional Categories
{Category: "Suppressions", SubCategory: "Unsubscribe Group Suppressions", Prefixes: []string{"asm.groups.suppressions"}},
{Category: "Suppressions", SubCategory: "Global Suppressions", Prefixes: []string{"asm.suppressions.global"}},
{Category: "Credentials", Prefixes: []string{"credentials"}},
{Category: "Mail Settings", Prefixes: []string{"mail_settings"}},
{Category: "Signup", Prefixes: []string{"signup"}},
{Category: "Suppressions", SubCategory: "Blocks", Prefixes: []string{"suppression.blocks"}},
{Category: "Suppressions", SubCategory: "Bounces", Prefixes: []string{"suppression.bounces"}},
{Category: "Suppressions", SubCategory: "Invalid Emails", Prefixes: []string{"suppression.invalid_emails"}},
{Category: "Suppressions", SubCategory: "Spam Reports", Prefixes: []string{"suppression.spam_reports"}},
{Category: "Suppressions", SubCategory: "Unsubscribes", Prefixes: []string{"suppression.unsubscribes"}},
{Category: "Teammates", Prefixes: []string{"teammates"}},
{Category: "Tracking", Prefixes: []string{"tracking_settings"}},
{Category: "UI", Prefixes: []string{"ui"}},
{Category: "User Account", SubCategory: "Account", Prefixes: []string{"user.account"}},
{Category: "User Account", SubCategory: "Credits", Prefixes: []string{"user.credits"}},
{Category: "User Account", SubCategory: "Email", Prefixes: []string{"user.email"}},
{Category: "User Account", SubCategory: "Multifactor Authentication", Prefixes: []string{"user.multifactor_authentication"}},
{Category: "User Account", SubCategory: "Password", Prefixes: []string{"user.password"}},
{Category: "User Account", SubCategory: "Profile", Prefixes: []string{"user.profile"}},
{Category: "User Account", SubCategory: "Username", Prefixes: []string{"user.username"}},
}
================================================
FILE: pkg/analyzer/analyzers/sendgrid/sendgrid.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go sendgrid
package sendgrid
import (
"encoding/json"
"fmt"
"os"
"slices"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
sg "github.com/sendgrid/sendgrid-go"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
type ScopesJSON struct {
Scopes []string `json:"scopes"`
}
type Profile struct {
ID int `json:"userid"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Company string `json:"company"`
Website string `json:"website"`
Country string `json:"country"`
}
type SecretInfo struct {
User Profile
RawScopes []string
Scopes []SendgridScope
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeSendgrid }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, fmt.Errorf("missing key in credInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[!] Error: %v", err)
return
}
color.Green("[!] Valid Sendgrid API Key\n\n")
if slices.Contains(info.RawScopes, "user.email.read") {
color.Green("[*] Sendgrid Key Type: Full Access Key")
} else if slices.Contains(info.RawScopes, "billing.read") {
color.Yellow("[*] Sendgrid Key Type: Billing Access Key")
} else {
color.Yellow("[*] Sendgrid Key Type: Restricted Access Key")
}
if slices.Contains(info.RawScopes, "2fa_required") {
color.Yellow("[i] 2FA Required for this account")
}
if info.User.FirstName != "" {
printProfile(info.User)
}
printPermissions(info, cfg.ShowAll)
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
// Setup custom HTTP client so we can log requests.
sg.DefaultClient.HTTPClient = analyzers.NewAnalyzeClient(cfg)
// get scopes
rawScopes, err := getScopes(key)
if err != nil {
return nil, err
}
categoryScope := processPermissions(rawScopes)
var secretInfo = &SecretInfo{
RawScopes: rawScopes,
Scopes: categoryScope,
}
if slices.Contains(rawScopes, "user.email.read") {
profile, err := getProfile(key)
if err != nil {
// if get profile fails return secretInfo with scopes for partial success
return secretInfo, nil
}
secretInfo.User = *profile
}
return secretInfo, nil
}
func getScopes(key string) ([]string, error) {
req := sg.GetRequest(key, "/v3/scopes", "https://api.sendgrid.com")
req.Method = "GET"
resp, err := sg.API(req)
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fmt.Errorf("invalid api key")
} else if resp.StatusCode != 200 {
return nil, fmt.Errorf("%v", resp.StatusCode)
}
if err != nil {
return nil, err
}
// Unmarshal the JSON response into a struct
var jsonScopes ScopesJSON
if err := json.Unmarshal([]byte(resp.Body), &jsonScopes); err != nil {
return nil, err
}
return jsonScopes.Scopes, nil
}
func getProfile(key string) (*Profile, error) {
req := sg.GetRequest(key, "/v3/user/profile", "https://api.sendgrid.com")
req.Method = "GET"
resp, err := sg.API(req)
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fmt.Errorf("invalid api key")
} else if resp.StatusCode != 200 {
return nil, fmt.Errorf("%v", resp.StatusCode)
}
if err != nil {
return nil, err
}
// Unmarshal the JSON response into a struct
var profile Profile
if err := json.Unmarshal([]byte(resp.Body), &profile); err != nil {
return nil, err
}
return &profile, nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
var keyType string
if slices.Contains(info.RawScopes, "user.email.read") {
keyType = "full access"
} else if slices.Contains(info.RawScopes, "billing.read") {
keyType = "billing access"
} else {
keyType = "restricted access"
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeSendgrid,
Metadata: map[string]any{
"key_type": keyType,
"2fa_required": slices.Contains(info.RawScopes, "2fa_required"),
},
Bindings: []analyzers.Binding{},
UnboundedResources: []analyzers.Resource{},
}
// add profile information to analyzer result
if info.User.ID != 0 && info.User.FirstName != "" {
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: analyzers.Resource{
Name: info.User.FirstName + " " + info.User.LastName,
FullyQualifiedName: fmt.Sprintf("%d", info.User.ID),
Type: "User",
},
Permission: analyzers.Permission{
Value: "full_access", // if token has all permissions than we can get user information
},
})
}
for _, scope := range info.Scopes {
resource := getCategoryResource(scope)
if len(scope.Permissions) == 0 {
result.UnboundedResources = append(result.UnboundedResources, *resource)
continue
}
for _, permission := range scope.Permissions {
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: *resource,
Permission: analyzers.Permission{
Value: permission,
},
})
}
}
return &result
}
func getCategoryResource(scope SendgridScope) *analyzers.Resource {
categoryResource := &analyzers.Resource{
Name: scope.Category,
FullyQualifiedName: scope.Category,
Type: "category",
Metadata: nil,
}
if scope.SubCategory != "" {
return &analyzers.Resource{
Name: scope.SubCategory,
FullyQualifiedName: fmt.Sprintf("%s/%s", scope.Category, scope.SubCategory),
Type: "category",
Metadata: nil,
Parent: categoryResource,
}
}
return categoryResource
}
// getCategoryFromScope returns the category for a given scope.
// It will return the most specific category possible.
// For example, if the scope is "mail.send.read", it will return "Mail Send", not just "Mail"
// since it's searching "mail.send.read" -> "mail.send" -> "mail"
func getScopeIndex(categories []SendgridScope, scope string) int {
splitScope := strings.Split(scope, ".")
for i := len(splitScope); i > 0; i-- {
searchScope := strings.Join(splitScope[:i], ".")
for i, s := range categories {
for _, prefix := range s.Prefixes {
if strings.HasPrefix(searchScope, prefix) {
return i
}
}
}
}
return -1
}
func processPermissions(rawScopes []string) []SendgridScope {
categoryPermissions := make([]SendgridScope, len(SCOPES))
// copy all scope categories to the categoryPermissions slice
copy(categoryPermissions, SCOPES)
for _, scope := range rawScopes {
// Skip these scopes since they are not useful for this analysis
if scope == "2fa_required" || scope == "sender_verification_eligible" {
continue
}
// must be part of generated permissions
if _, ok := StringToPermission[scope]; !ok {
continue
}
ind := getScopeIndex(categoryPermissions, scope)
if ind == -1 {
//color.Red("[!] Scope not found: %v", scope)
continue
}
s := &categoryPermissions[ind]
s.AddPermission(scope)
}
// Run tests to determine the permission type
for i := range categoryPermissions {
categoryPermissions[i].RunTests()
}
return categoryPermissions
}
func printProfile(profile Profile) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"UserID", "Name", "Company", "Website", "Country"})
t.AppendRow(table.Row{profile.ID, profile.FirstName + " " + profile.LastName, profile.Company, profile.Website, profile.Country})
t.Render()
}
func printPermissions(info *SecretInfo, show_all bool) {
fmt.Print("\n\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
if show_all {
t.AppendHeader(table.Row{"Scope", "Sub-Scope", "Access", "Permissions"})
} else {
t.AppendHeader(table.Row{"Scope", "Sub-Scope", "Access"})
}
// Print the scopes
for _, s := range info.Scopes {
writer := analyzers.GetWriterFromStatus(s.PermissionType)
if show_all {
t.AppendRow([]interface{}{writer(s.Category), writer(s.SubCategory), writer(s.PermissionType), writer(strings.Join(s.Permissions, "\n"))})
} else if s.PermissionType != analyzers.NONE {
t.AppendRow([]interface{}{writer(s.Category), writer(s.SubCategory), writer(s.PermissionType)})
}
}
t.Render()
fmt.Print("\n\n")
}
================================================
FILE: pkg/analyzer/analyzers/sendgrid/sendgrid_test.go
================================================
package sendgrid
import (
_ "embed"
"encoding/json"
"fmt"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed result_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(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)
}
tests := []struct {
name string
key string
want []byte // JSON string
wantErr bool
}{
{
name: "Valid Sendgrid key",
key: testSecrets.MustGetField("SENDGRID"),
want: expectedOutput,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
fmt.Println(string(gotJSON))
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/shopify/expected_output.json
================================================
{
"AnalyzerType": 15,
"Bindings": [
{
"Resource": {
"Name": "Analytics",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/Analytics",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "My Store",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
"Type": "shop",
"Metadata": {
"created_at": "2024-08-16T17:16:17+05:00"
},
"Parent": null
}
},
"Permission": {
"Value": "read",
"Parent": null
}
},
{
"Resource": {
"Name": "Applications",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/Applications",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "My Store",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
"Type": "shop",
"Metadata": {
"created_at": "2024-08-16T17:16:17+05:00"
},
"Parent": null
}
},
"Permission": {
"Value": "read",
"Parent": null
}
},
{
"Resource": {
"Name": "Assigned fulfillment orders",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/Assigned fulfillment orders",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "My Store",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
"Type": "shop",
"Metadata": {
"created_at": "2024-08-16T17:16:17+05:00"
},
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Customers",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/Customers",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "My Store",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
"Type": "shop",
"Metadata": {
"created_at": "2024-08-16T17:16:17+05:00"
},
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Discovery",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/Discovery",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "My Store",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
"Type": "shop",
"Metadata": {
"created_at": "2024-08-16T17:16:17+05:00"
},
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Merchant-managed fulfillment orders",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/Merchant-managed fulfillment orders",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "My Store",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
"Type": "shop",
"Metadata": {
"created_at": "2024-08-16T17:16:17+05:00"
},
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Reports",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/Reports",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "My Store",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
"Type": "shop",
"Metadata": {
"created_at": "2024-08-16T17:16:17+05:00"
},
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
},
{
"Resource": {
"Name": "cart_transforms",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/cart_transforms",
"Type": "category",
"Metadata": null,
"Parent": {
"Name": "My Store",
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
"Type": "shop",
"Metadata": {
"created_at": "2024-08-16T17:16:17+05:00"
},
"Parent": null
}
},
"Permission": {
"Value": "full_access",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": {
"status_code": 200
}
}
================================================
FILE: pkg/analyzer/analyzers/shopify/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package shopify
import "errors"
type Permission int
const (
Invalid Permission = iota
Read Permission = iota
Write Permission = iota
FullAccess Permission = iota
)
var (
PermissionStrings = map[Permission]string{
Read: "read",
Write: "write",
FullAccess: "full_access",
}
StringToPermission = map[string]Permission{
"read": Read,
"write": Write,
"full_access": FullAccess,
}
PermissionIDs = map[Permission]int{
Read: 1,
Write: 2,
FullAccess: 3,
}
IdToPermission = map[int]Permission{
1: Read,
2: Write,
3: FullAccess,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/shopify/permissions.yaml
================================================
permissions:
- read
- write
- full_access
================================================
FILE: pkg/analyzer/analyzers/shopify/scopes.json
================================================
{
"categories": {
"Analytics": {
"description": "View store metrics",
"scopes": {
"read_analytics": "Read"
}
},
"Applications": {
"description": "View or manage apps",
"scopes": {
"read_apps": "Read"
}
},
"Assigned fulfillment orders": {
"description": "View or manage fulfillment orders",
"scopes": {
"write_assigned_fulfillment_orders": "Write",
"read_assigned_fulfillment_orders": "Read"
}
},
"Browsing behavior": {
"description": "View or manage online-store browsing behavior including page views, cart updates, product views and searches",
"scopes": {
"read_customer_events": "Read"
}
},
"Custom pixels": {
"description": "View or manage custom pixels",
"scopes": {
"write_custom_pixels": "Write",
"read_custom_pixels": "Read"
}
},
"Customers": {
"description": "View or manage customers, customer addresses, order history, and customer groups",
"scopes": {
"write_customers": "Write",
"read_customers": "Read"
}
},
"Discounts": {
"description": "View or manage automatic discounts and discount codes",
"scopes": {
"write_discounts": "Write",
"read_discounts": "Read"
}
},
"Discovery": {
"description": "View or manage Discovery API",
"scopes": {
"write_discovery": "Write",
"read_discovery": "Read"
}
},
"Draft orders": {
"description": "View or manage orders created by merchants on behalf of customers",
"scopes": {
"write_draft_orders": "Write",
"read_draft_orders": "Read"
}
},
"Files": {
"description": "View or manage files",
"scopes": {
"write_files": "Write",
"read_files": "Read"
}
},
"Fulfillment services": {
"description": "View or manage fulfillment services",
"scopes": {
"write_fulfillments": "Write",
"read_fulfillments": "Read"
}
},
"Gift cards": {
"description": "View or manage gift cards",
"scopes": {
"write_gift_cards": "Write",
"read_gift_cards": "Read"
}
},
"Inventory": {
"description": "View or manage inventory across multiple locations",
"scopes": {
"write_inventory": "Write",
"read_inventory": "Read"
}
},
"Legal policies": {
"description": "View or manage a shop's legal policies",
"scopes": {
"write_legal_policies": "Write",
"read_legal_policies": "Read"
}
},
"Locations": {
"description": "View the geographic location of stores, headquarters, and warehouses",
"scopes": {
"write_locations": "Write",
"read_locations": "Read"
}
},
"Marketing events": {
"description": "View or manage marketing events and engagement data",
"scopes": {
"write_marketing_events": "Write",
"read_marketing_events": "Read"
}
},
"Merchant-managed fulfillment orders": {
"description": "View or manage fulfillment orders assigned to merchant-managed locations",
"scopes": {
"write_merchant_managed_fulfillment_orders": "Write",
"read_merchant_managed_fulfillment_orders": "Read"
}
},
"Metaobject definitions": {
"description": "View or manage definitions",
"scopes": {
"write_metaobject_definitions": "Write",
"read_metaobject_definitions": "Read"
}
},
"Metaobject entries": {
"description": "View or manage entries",
"scopes": {
"write_metaobjects": "Write",
"read_metaobjects": "Read"
}
},
"Online Store navigation": {
"description": "View menus for display on the storefront",
"scopes": {
"write_online_store_navigation": "Write",
"read_online_store_navigation": "Read"
}
},
"Online Store pages": {
"description": "View or manage Online Store pages",
"scopes": {
"write_online_store_pages": "Write",
"read_online_store_pages": "Read"
}
},
"Order editing": {
"description": "View or manage edits to orders",
"scopes": {
"write_order_edits": "Write",
"read_order_edits": "Read"
}
},
"Orders": {
"description": "View or manage orders, transactions, fulfillments, and abandoned checkouts",
"scopes": {
"write_orders": "Write",
"read_orders": "Read"
}
},
"Packing slip management": {
"description": "Edit and preview packing slip template",
"scopes": {
"write_packing_slip_templates": "Write",
"read_packing_slip_templates": "Read"
}
},
"Payment customizations": {
"description": "View or manage payment customizations",
"scopes": {
"write_payment_customizations": "Write",
"read_payment_customizations": "Read"
}
},
"Payment terms": {
"description": "View or manage payment terms",
"scopes": {
"write_payment_terms": "Write",
"read_payment_terms": "Read"
}
},
"Pixels": {
"description": "View or manage pixels",
"scopes": {
"write_pixels": "Write",
"read_pixels": "Read"
}
},
"Price rules": {
"description": "View or manage conditional discounts",
"scopes": {
"write_price_rules": "Write",
"read_price_rules": "Read"
}
},
"Product feeds": {
"description": "View or manage product feeds",
"scopes": {
"write_product_feeds": "Write",
"read_product_feeds": "Read"
}
},
"Product listings": {
"description": "View or manage product or collection listings",
"scopes": {
"write_product_listings": "Write",
"read_product_listings": "Read"
}
},
"Products": {
"description": "View or manage products, variants, and collections",
"scopes": {
"write_products": "Write",
"read_products": "Read"
}
},
"Publications": {
"description": "View or manage groups of products that have been published to an app",
"scopes": {
"write_publications": "Write",
"read_publications": "Read"
}
},
"Purchase options": {
"description": "View or manage purchase options owned by this app",
"scopes": {
"write_purchase_options": "Write",
"read_purchase_options": "Read"
}
},
"Reports": {
"description": "View or manage reports on the Reports page in the Shopify admin",
"scopes": {
"write_reports": "Write",
"read_reports": "Read"
}
},
"Resource feedback": {
"description": "View or manage the status of shops and resources",
"scopes": {
"write_resource_feedbacks": "Write",
"read_resource_feedbacks": "Read"
}
},
"Returns": {
"description": "View or manage returns",
"scopes": {
"write_returns": "Write",
"read_returns": "Read"
}
},
"Sales channels": {
"description": "View or manage sales channels",
"scopes": {
"write_channels": "Write",
"read_channels": "Read"
}
},
"Script tags": {
"description": "View or manage the JavaScript code in storefront or orders status pages",
"scopes": {
"write_script_tags": "Write",
"read_script_tags": "Read"
}
},
"Shipping": {
"description": "View or manage shipping carriers, countries, and provinces",
"scopes": {
"write_shipping": "Write",
"read_shipping": "Read"
}
},
"Shop locales": {
"description": "View or manage available locales for a shop",
"scopes": {
"write_locales": "Write",
"read_locales": "Read"
}
},
"Shopify Markets": {
"description": "View or manage Shopify Markets configuration",
"scopes": {
"write_markets": "Write",
"read_markets": "Read"
}
},
"Shopify Payments accounts": {
"description": "View Shopify Payments accounts",
"scopes": {
"read_shopify_payments_accounts": "Read"
}
},
"Shopify Payments bank accounts": {
"description": "View bank accounts that can receive Shopify Payment payouts",
"scopes": {
"read_shopify_payments_bank_accounts": "Read"
}
},
"Shopify Payments disputes": {
"description": "View Shopify Payment disputes raised by buyers",
"scopes": {
"write_shopify_payments_disputes": "Write",
"read_shopify_payments_disputes": "Read"
}
},
"Shopify Payments payouts": {
"description": "View Shopify Payments payouts and the account's current balance",
"scopes": {
"read_shopify_payments_payouts": "Read"
}
},
"Store content": {
"description": "View or manage articles, blogs, comments, pages, and redirects",
"scopes": {
"write_content": "Write",
"read_content": "Read"
}
},
"Store credit account transactions": {
"description": "View or create store credit transactions",
"scopes": {
"write_store_credit_account_transactions": "Write",
"read_store_credit_account_transactions": "Read"
}
},
"Store credit accounts": {
"description": "View a customer's store credit balance and currency",
"scopes": {
"read_store_credit_accounts": "Read"
}
},
"Themes": {
"description": "View or manage theme templates and assets",
"scopes": {
"write_themes": "Write",
"read_themes": "Read"
}
},
"Third-party fulfillment orders": {
"description": "View or manage fulfillment orders assigned to a location managed by any fulfillment service",
"scopes": {
"write_third_party_fulfillment_orders": "Write",
"read_third_party_fulfillment_orders": "Read"
}
},
"Translations": {
"description": "View or manage content that can be translated",
"scopes": {
"write_translations": "Write",
"read_translations": "Read"
}
},
"all_cart_transforms": {
"description": "",
"scopes": {
"read_all_cart_transforms": "Read"
}
},
"all_checkout_completion_target_customizations": {
"description": "",
"scopes": {
"write_all_checkout_completion_target_customizations": "Write",
"read_all_checkout_completion_target_customizations": "Read"
}
},
"cart_transforms": {
"description": "",
"scopes": {
"write_cart_transforms": "Write",
"read_cart_transforms": "Read"
}
},
"cash_tracking": {
"description": "",
"scopes": {
"read_cash_tracking": "Read"
}
},
"companies": {
"description": "",
"scopes": {
"write_companies": "Write",
"read_companies": "Read"
}
},
"custom_fulfillment_services": {
"description": "",
"scopes": {
"write_custom_fulfillment_services": "Write",
"read_custom_fulfillment_services": "Read"
}
},
"customer_data_erasure": {
"description": "",
"scopes": {
"write_customer_data_erasure": "Write",
"read_customer_data_erasure": "Read"
}
},
"customer_merge": {
"description": "",
"scopes": {
"write_customer_merge": "Write",
"read_customer_merge": "Read"
}
},
"delivery_customizations": {
"description": "",
"scopes": {
"write_delivery_customizations": "Write",
"read_delivery_customizations": "Read"
}
},
"delivery_option_generators": {
"description": "",
"scopes": {
"write_delivery_option_generators": "Write",
"read_delivery_option_generators": "Read"
}
},
"discounts_allocator_functions": {
"description": "",
"scopes": {
"write_discounts_allocator_functions": "Write",
"read_discounts_allocator_functions": "Read"
}
},
"fulfillment_constraint_rules": {
"description": "",
"scopes": {
"write_fulfillment_constraint_rules": "Write",
"read_fulfillment_constraint_rules": "Read"
}
},
"gates": {
"description": "",
"scopes": {
"write_gates": "Write",
"read_gates": "Read"
}
},
"order_submission_rules": {
"description": "",
"scopes": {
"write_order_submission_rules": "Write",
"read_order_submission_rules": "Read"
}
},
"privacy_settings": {
"description": "",
"scopes": {
"write_privacy_settings": "Write",
"read_privacy_settings": "Read"
}
},
"shopify_payments_provider_accounts_sensitive": {
"description": "",
"scopes": {
"read_shopify_payments_provider_accounts_sensitive": "Read"
}
},
"validations": {
"description": "",
"scopes": {
"write_validations": "Write",
"read_validations": "Read"
}
}
}
}
================================================
FILE: pkg/analyzer/analyzers/shopify/shopify.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go shopify
package shopify
import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
var (
// order the categories
categoryOrder = []string{"Analytics", "Applications", "Assigned fulfillment orders", "Browsing behavior", "Custom pixels", "Customers", "Discounts", "Discovery", "Draft orders", "Files", "Fulfillment services", "Gift cards", "Inventory", "Legal policies", "Locations", "Marketing events", "Merchant-managed fulfillment orders", "Metaobject definitions", "Metaobject entries", "Online Store navigation", "Online Store pages", "Order editing", "Orders", "Packing slip management", "Payment customizations", "Payment terms", "Pixels", "Price rules", "Product feeds", "Product listings", "Products", "Publications", "Purchase options", "Reports", "Resource feedback", "Returns", "Sales channels", "Script tags", "Shipping", "Shop locales", "Shopify Markets", "Shopify Payments accounts", "Shopify Payments bank accounts", "Shopify Payments disputes", "Shopify Payments payouts", "Store content", "Store credit account transactions", "Store credit accounts", "Themes", "Third-party fulfillment orders", "Translations", "all_cart_transforms", "all_checkout_completion_target_customizations", "cart_transforms", "cash_tracking", "companies", "custom_fulfillment_services", "customer_data_erasure", "customer_merge", "delivery_customizations", "delivery_option_generators", "discounts_allocator_functions", "fulfillment_constraint_rules", "gates", "order_submission_rules", "privacy_settings", "shopify_payments_provider_accounts_sensitive", "validations"}
)
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeShopify }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, errors.New("key not found in credentialInfo")
}
storeUrl, ok := credInfo["store_url"]
if !ok {
return nil, errors.New("store_url not found in credentialInfo")
}
info, err := AnalyzePermissions(a.Cfg, key, storeUrl)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeShopify,
Metadata: map[string]any{
"status_code": info.StatusCode,
},
}
resource := &analyzers.Resource{
Name: info.ShopInfo.Shop.Name,
FullyQualifiedName: info.ShopInfo.Shop.Domain + "/" + info.ShopInfo.Shop.Email,
Type: "shop",
Metadata: map[string]any{
"created_at": info.ShopInfo.Shop.CreatedAt,
},
Parent: nil,
}
result.Bindings = make([]analyzers.Binding, 0)
for _, category := range categoryOrder {
if val, ok := info.Scopes[category]; ok {
cateogryResource := &analyzers.Resource{
Name: category,
FullyQualifiedName: resource.FullyQualifiedName + "/" + category, // shop.domain/shop.email/category
Type: "category",
Parent: resource,
}
if sliceContains(val.Scopes, "Read") && sliceContains(val.Scopes, "Write") {
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: *cateogryResource,
Permission: analyzers.Permission{
Value: PermissionStrings[FullAccess],
},
})
continue
}
for _, scope := range val.Scopes {
lowerScope := strings.ToLower(scope)
if _, ok := StringToPermission[lowerScope]; !ok { // skip unknown scopes/permission
continue
}
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: *cateogryResource,
Permission: analyzers.Permission{
Value: lowerScope,
},
})
}
}
}
return &result
}
//go:embed scopes.json
var scopesConfig []byte
func sliceContains(slice []string, value string) bool {
for _, v := range slice {
if v == value {
return true
}
}
return false
}
type OutputScopes struct {
Description string
Scopes []string
}
func (o OutputScopes) PrintScopes() string {
// Custom rules unique to this analyzer
var scopes []string
if sliceContains(o.Scopes, "Read") && sliceContains(o.Scopes, "Write") {
scopes = append(scopes, "Read & Write")
for _, scope := range o.Scopes {
if scope != "Read" && scope != "Write" {
scopes = append(scopes, scope)
}
}
} else {
scopes = append(scopes, o.Scopes...)
}
return strings.Join(scopes, ", ")
}
// Category represents the structure of each category in the JSON
type CategoryJSON struct {
Description string `json:"description"`
Scopes map[string]string `json:"scopes"`
}
// Data represents the overall JSON structure
type ScopeDataJSON struct {
Categories map[string]CategoryJSON `json:"categories"`
}
// Function to determine the appropriate scope
func determineScopes(data ScopeDataJSON, input string) map[string]OutputScopes {
// Split the input string into individual scopes
inputScopes := strings.Split(input, ", ")
// Map to store scopes found for each category
scopeResults := make(map[string]OutputScopes)
// Populate categoryScopes map with individual scopes found
for _, scope := range inputScopes {
for category, catData := range data.Categories {
if scopeType, exists := catData.Scopes[scope]; exists {
if _, ok := scopeResults[category]; !ok {
scopeResults[category] = OutputScopes{Description: catData.Description}
}
// Extract the struct from the map
outputData := scopeResults[category]
// Modify the struct (ex: append "Read" or "Write" to the Scopes slice)
outputData.Scopes = append(outputData.Scopes, scopeType)
// Reassign the modified struct back to the map
scopeResults[category] = outputData
}
}
}
return scopeResults
}
type ShopInfoJSON struct {
Shop struct {
Domain string `json:"domain"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt string `json:"created_at"`
} `json:"shop"`
}
type SecretInfo struct {
StatusCode int
ShopInfo ShopInfoJSON
Scopes map[string]OutputScopes
}
func getShopInfo(cfg *config.Config, key string, store string) (ShopInfoJSON, error) {
var shopInfo ShopInfoJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/admin/api/2024-04/shop.json", store), nil)
if err != nil {
return shopInfo, err
}
req.Header.Set("X-Shopify-Access-Token", key)
resp, err := client.Do(req)
if err != nil {
return shopInfo, err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&shopInfo)
if err != nil {
return shopInfo, err
}
return shopInfo, nil
}
type AccessScopesJSON struct {
AccessScopes []struct {
Handle string `json:"handle"`
} `json:"access_scopes"`
}
func (a AccessScopesJSON) String() string {
var scopes []string
for _, scope := range a.AccessScopes {
scopes = append(scopes, scope.Handle)
}
return strings.Join(scopes, ", ")
}
func getAccessScopes(cfg *config.Config, key string, store string) (AccessScopesJSON, int, error) {
var accessScopes AccessScopesJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/admin/oauth/access_scopes.json", store), nil)
if err != nil {
return accessScopes, -1, err
}
req.Header.Set("X-Shopify-Access-Token", key)
resp, err := client.Do(req)
if err != nil {
return accessScopes, resp.StatusCode, err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&accessScopes)
if err != nil {
return accessScopes, resp.StatusCode, err
}
return accessScopes, resp.StatusCode, nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string, storeURL string) {
// ToDo: Add in logging
if cfg.LoggingEnabled {
color.Red("[x] Logging is not supported for this analyzer.")
return
}
info, err := AnalyzePermissions(cfg, key, storeURL)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
if info.StatusCode != 200 {
color.Red("[x] Invalid Shopfiy API Key and Store URL combination")
return
}
color.Green("[i] Valid Shopify API Key\n\n")
color.Yellow("[i] Shop Information\n")
color.Yellow("Name: %s", info.ShopInfo.Shop.Name)
color.Yellow("Email: %s", info.ShopInfo.Shop.Email)
color.Yellow("Created At: %s\n\n", info.ShopInfo.Shop.CreatedAt)
printAccessScopes(info.Scopes)
}
func AnalyzePermissions(cfg *config.Config, key string, storeURL string) (*SecretInfo, error) {
accessScopes, statusCode, err := getAccessScopes(cfg, key, storeURL)
if err != nil {
return nil, err
}
shopInfo, err := getShopInfo(cfg, key, storeURL)
if err != nil {
return nil, err
}
var data ScopeDataJSON
if err := json.Unmarshal(scopesConfig, &data); err != nil {
return nil, err
}
scopes := determineScopes(data, accessScopes.String())
return &SecretInfo{
StatusCode: statusCode,
ShopInfo: shopInfo,
Scopes: scopes,
}, nil
}
func printAccessScopes(accessScopes map[string]OutputScopes) {
color.Yellow("[i] Access Scopes\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "Description", "Access"})
for _, category := range categoryOrder {
if val, ok := accessScopes[category]; ok {
t.AppendRow([]interface{}{color.GreenString(category), color.GreenString(val.Description), color.GreenString(val.PrintScopes())})
}
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/shopify/shopify_test.go
================================================
package shopify
import (
_ "embed"
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("SHOPIFY_ADMIN_SECRET")
domain := testSecrets.MustGetField("SHOPIFY_DOMAIN")
tests := []struct {
name string
key string
storeUrl string
want string
wantErr bool
}{
{
name: "valid Shopify key",
key: secret,
storeUrl: domain,
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key, "store_url": tt.storeUrl})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/slack/expected_output.json
================================================
{
"AnalyzerType": 16,
"Bindings": [
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "conversations.history",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "conversations.replies",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "channels.info",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "conversations.info",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "conversations.list",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "conversations.members",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "groups.info",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "im.list",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "mpim.list",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "users.conversations",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "emoji.list",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "files.info",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "files.list",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "stars.list",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "pins.list",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "usergroups.list",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "usergroups.users.list",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "dnd.info",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "dnd.teamInfo",
"Parent": null
}
},
{
"Resource": {
"Name": "marge.haskell.bridge",
"FullyQualifiedName": "TSMCXP5FH/USMD5JM0F",
"Type": "user",
"Metadata": {
"scopes": [
"identify",
"channels:history",
"groups:history",
"im:history",
"channels:read",
"emoji:read",
"files:read",
"groups:read",
"im:read",
"stars:read",
"pins:read",
"usergroups:read",
"dnd:read",
"calls:read"
],
"team": "ct.org",
"team_id": "TSMCXP5FH",
"url": "https://ctorgworkspace.slack.com/"
},
"Parent": null
},
"Permission": {
"Value": "calls.info",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": null
}
================================================
FILE: pkg/analyzer/analyzers/slack/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package slack
import "errors"
type Permission int
const (
Invalid Permission = iota
AdminAnalyticsRead Permission = iota
AdminAnalyticsGetfile Permission = iota
AdminAppActivitiesRead Permission = iota
AdminAppsActivitiesList Permission = iota
AdminAppsWrite Permission = iota
AdminAppsApprove Permission = iota
AdminAppsClearresolution Permission = iota
AdminAppsConfigSet Permission = iota
AdminAppsRequestsCancel Permission = iota
AdminAppsRestrict Permission = iota
AdminAppsUninstall Permission = iota
AdminAppsRead Permission = iota
AdminAppsApprovedList Permission = iota
AdminAppsConfigLookup Permission = iota
AdminAppsRequestsList Permission = iota
AdminAppsRestrictedList Permission = iota
AdminUsersWrite Permission = iota
AdminAuthPolicyAssignentities Permission = iota
AdminAuthPolicyRemoveentities Permission = iota
AdminUsersAssign Permission = iota
AdminUsersInvite Permission = iota
AdminUsersRemove Permission = iota
AdminUsersSessionClearsettings Permission = iota
AdminUsersSessionInvalidate Permission = iota
AdminUsersSessionReset Permission = iota
AdminUsersSessionResetbulk Permission = iota
AdminUsersSessionSetsettings Permission = iota
AdminUsersSetadmin Permission = iota
AdminUsersSetexpiration Permission = iota
AdminUsersSetowner Permission = iota
AdminUsersSetregular Permission = iota
AdminUsersRead Permission = iota
AdminAuthPolicyGetentities Permission = iota
AdminUsersList Permission = iota
AdminUsersSessionGetsettings Permission = iota
AdminUsersSessionList Permission = iota
AdminUsersUnsupportedversionsExport Permission = iota
AdminBarriersWrite Permission = iota
AdminBarriersCreate Permission = iota
AdminBarriersDelete Permission = iota
AdminBarriersUpdate Permission = iota
AdminBarriersRead Permission = iota
AdminBarriersList Permission = iota
AdminConversationsWrite Permission = iota
AdminConversationsArchive Permission = iota
AdminConversationsBulkarchive Permission = iota
AdminConversationsBulkdelete Permission = iota
AdminConversationsBulkmove Permission = iota
AdminConversationsConverttoprivate Permission = iota
AdminConversationsConverttopublic Permission = iota
AdminConversationsCreate Permission = iota
AdminConversationsDelete Permission = iota
AdminConversationsDisconnectshared Permission = iota
AdminConversationsInvite Permission = iota
AdminConversationsRemovecustomretention Permission = iota
AdminConversationsRename Permission = iota
AdminConversationsRestrictaccessAddgroup Permission = iota
AdminConversationsRestrictaccessRemovegroup Permission = iota
AdminConversationsSetconversationprefs Permission = iota
AdminConversationsSetcustomretention Permission = iota
AdminConversationsSetteams Permission = iota
AdminConversationsUnarchive Permission = iota
AdminConversationsRead Permission = iota
AdminConversationsEkmListoriginalconnectedchannelinfo Permission = iota
AdminConversationsGetconversationprefs Permission = iota
AdminConversationsGetcustomretention Permission = iota
AdminConversationsGetteams Permission = iota
AdminConversationsLookup Permission = iota
AdminConversationsRestrictaccessListgroups Permission = iota
AdminConversationsSearch Permission = iota
AdminTeamsWrite Permission = iota
AdminEmojiAdd Permission = iota
AdminEmojiAddalias Permission = iota
AdminEmojiRemove Permission = iota
AdminTeamsCreate Permission = iota
AdminTeamsSettingsSetdefaultchannels Permission = iota
AdminTeamsSettingsSetdescription Permission = iota
AdminTeamsSettingsSetdiscoverability Permission = iota
AdminTeamsSettingsSeticon Permission = iota
AdminTeamsSettingsSetname Permission = iota
AdminUsergroupsAddteams Permission = iota
AdminTeamsRead Permission = iota
AdminEmojiList Permission = iota
AdminTeamsAdminsList Permission = iota
AdminTeamsList Permission = iota
AdminTeamsOwnersList Permission = iota
AdminTeamsSettingsInfo Permission = iota
AdminWorkflowsRead Permission = iota
AdminFunctionsList Permission = iota
AdminFunctionsPermissionsLookup Permission = iota
AdminWorkflowsPermissionsLookup Permission = iota
AdminWorkflowsSearch Permission = iota
AdminWorkflowsWrite Permission = iota
AdminFunctionsPermissionsSet Permission = iota
AdminWorkflowsCollaboratorsAdd Permission = iota
AdminWorkflowsCollaboratorsRemove Permission = iota
AdminWorkflowsUnpublish Permission = iota
AdminInvitesWrite Permission = iota
AdminInviterequestsApprove Permission = iota
AdminInviterequestsDeny Permission = iota
AdminInvitesRead Permission = iota
AdminInviterequestsApprovedList Permission = iota
AdminInviterequestsDeniedList Permission = iota
AdminInviterequestsList Permission = iota
AdminRolesWrite Permission = iota
AdminRolesAddassignments Permission = iota
AdminRolesRemoveassignments Permission = iota
AdminRolesRead Permission = iota
AdminRolesListassignments Permission = iota
AdminUsergroupsWrite Permission = iota
AdminUsergroupsAddchannels Permission = iota
AdminUsergroupsRemovechannels Permission = iota
AdminUsergroupsRead Permission = iota
AdminUsergroupsListchannels Permission = iota
HostingRead Permission = iota
AppsActivitiesList Permission = iota
ConnectionsWrite Permission = iota
AppsConnectionsOpen Permission = iota
Token Permission = iota
AppsDatastoreBulkdelete Permission = iota
AppsDatastoreBulkget Permission = iota
AppsDatastoreBulkput Permission = iota
AppsDatastoreDelete Permission = iota
AppsDatastoreGet Permission = iota
AppsDatastorePut Permission = iota
AppsDatastoreQuery Permission = iota
AppsDatastoreUpdate Permission = iota
DatastoreRead Permission = iota
AppsDatastoreCount Permission = iota
AuthorizationsRead Permission = iota
AppsEventAuthorizationsList Permission = iota
Bot Permission = iota
AuthRevoke Permission = iota
AuthTest Permission = iota
ChatGetpermalink Permission = iota
ChatScheduledmessagesList Permission = iota
DialogOpen Permission = iota
FunctionsCompleteerror Permission = iota
FunctionsCompletesuccess Permission = iota
RtmConnect Permission = iota
RtmStart Permission = iota
ViewsOpen Permission = iota
ViewsPublish Permission = iota
ViewsPush Permission = iota
ViewsUpdate Permission = iota
BookmarksWrite Permission = iota
BookmarksAdd Permission = iota
BookmarksEdit Permission = iota
BookmarksRemove Permission = iota
BookmarksRead Permission = iota
BookmarksList Permission = iota
UsersRead Permission = iota
BotsInfo Permission = iota
UsersGetpresence Permission = iota
UsersInfo Permission = iota
UsersList Permission = iota
CallsWrite Permission = iota
CallsAdd Permission = iota
CallsEnd Permission = iota
CallsParticipantsAdd Permission = iota
CallsParticipantsRemove Permission = iota
CallsUpdate Permission = iota
CallsRead Permission = iota
CallsInfo Permission = iota
ChannelsManage Permission = iota
ChannelsCreate Permission = iota
ChannelsMark Permission = iota
ConversationsArchive Permission = iota
ConversationsClose Permission = iota
ConversationsCreate Permission = iota
ConversationsKick Permission = iota
ConversationsLeave Permission = iota
ConversationsMark Permission = iota
ConversationsOpen Permission = iota
ConversationsRename Permission = iota
ConversationsUnarchive Permission = iota
GroupsCreate Permission = iota
GroupsMark Permission = iota
ImMark Permission = iota
ImOpen Permission = iota
MpimMark Permission = iota
MpimOpen Permission = iota
ChannelsRead Permission = iota
ChannelsInfo Permission = iota
ConversationsInfo Permission = iota
ConversationsList Permission = iota
ConversationsMembers Permission = iota
GroupsInfo Permission = iota
ImList Permission = iota
MpimList Permission = iota
UsersConversations Permission = iota
ChannelsWriteInvites Permission = iota
ChannelsInvite Permission = iota
ConversationsInvite Permission = iota
GroupsInvite Permission = iota
ChatWrite Permission = iota
ChatDelete Permission = iota
ChatDeletescheduledmessage Permission = iota
ChatMemessage Permission = iota
ChatPostephemeral Permission = iota
ChatPostmessage Permission = iota
ChatSchedulemessage Permission = iota
ChatUpdate Permission = iota
LinksWrite Permission = iota
ChatUnfurl Permission = iota
ConversationsConnectWrite Permission = iota
ConversationsAcceptsharedinvite Permission = iota
ConversationsInviteshared Permission = iota
ConversationsConnectManage Permission = iota
ConversationsApprovesharedinvite Permission = iota
ConversationsDeclinesharedinvite Permission = iota
ConversationsListconnectinvites Permission = iota
ChannelsHistory Permission = iota
ConversationsHistory Permission = iota
ConversationsReplies Permission = iota
ChannelsJoin Permission = iota
ConversationsJoin Permission = iota
ChannelsWriteTopic Permission = iota
ConversationsSetpurpose Permission = iota
ConversationsSettopic Permission = iota
DndWrite Permission = iota
DndEnddnd Permission = iota
DndEndsnooze Permission = iota
DndSetsnooze Permission = iota
DndRead Permission = iota
DndInfo Permission = iota
DndTeaminfo Permission = iota
EmojiRead Permission = iota
EmojiList Permission = iota
FilesWrite Permission = iota
FilesCommentsDelete Permission = iota
FilesCompleteuploadexternal Permission = iota
FilesDelete Permission = iota
FilesGetuploadurlexternal Permission = iota
FilesRevokepublicurl Permission = iota
FilesSharedpublicurl Permission = iota
FilesUpload Permission = iota
FilesRead Permission = iota
FilesInfo Permission = iota
FilesList Permission = iota
RemoteFilesWrite Permission = iota
FilesRemoteAdd Permission = iota
FilesRemoteRemove Permission = iota
FilesRemoteUpdate Permission = iota
RemoteFilesRead Permission = iota
FilesRemoteInfo Permission = iota
FilesRemoteList Permission = iota
RemoteFilesShare Permission = iota
FilesRemoteShare Permission = iota
AppConfigurationsWrite Permission = iota
FunctionsDistributionsPermissionsAdd Permission = iota
FunctionsDistributionsPermissionsRemove Permission = iota
FunctionsDistributionsPermissionsSet Permission = iota
AppConfigurationsRead Permission = iota
FunctionsDistributionsPermissionsList Permission = iota
Conversations Permission = iota
GroupsOpen Permission = iota
TokensBasic Permission = iota
MigrationExchange Permission = iota
Email Permission = iota
OpenidConnectUserinfo Permission = iota
PinsWrite Permission = iota
PinsAdd Permission = iota
PinsRemove Permission = iota
PinsRead Permission = iota
PinsList Permission = iota
ReactionsWrite Permission = iota
ReactionsAdd Permission = iota
ReactionsRemove Permission = iota
ReactionsRead Permission = iota
ReactionsGet Permission = iota
ReactionsList Permission = iota
RemindersWrite Permission = iota
RemindersAdd Permission = iota
RemindersComplete Permission = iota
RemindersDelete Permission = iota
RemindersRead Permission = iota
RemindersInfo Permission = iota
RemindersList Permission = iota
SearchRead Permission = iota
SearchAll Permission = iota
SearchFiles Permission = iota
SearchMessages Permission = iota
StarsWrite Permission = iota
StarsAdd Permission = iota
StarsRemove Permission = iota
StarsRead Permission = iota
StarsList Permission = iota
Admin Permission = iota
TeamAccesslogs Permission = iota
TeamBillableinfo Permission = iota
TeamIntegrationlogs Permission = iota
TeamBillingRead Permission = iota
TeamBillingInfo Permission = iota
TeamRead Permission = iota
TeamInfo Permission = iota
TeamPreferencesRead Permission = iota
TeamPreferencesList Permission = iota
UsersProfileRead Permission = iota
TeamProfileGet Permission = iota
UsersProfileGet Permission = iota
UsergroupsWrite Permission = iota
UsergroupsCreate Permission = iota
UsergroupsDisable Permission = iota
UsergroupsEnable Permission = iota
UsergroupsUpdate Permission = iota
UsergroupsUsersUpdate Permission = iota
UsergroupsRead Permission = iota
UsergroupsList Permission = iota
UsergroupsUsersList Permission = iota
UsersProfileWrite Permission = iota
UsersDeletephoto Permission = iota
UsersProfileSet Permission = iota
UsersSetphoto Permission = iota
IdentityBasic Permission = iota
UsersIdentity Permission = iota
UsersReadEmail Permission = iota
UsersLookupbyemail Permission = iota
UsersWrite Permission = iota
UsersSetactive Permission = iota
UsersSetpresence Permission = iota
WorkflowStepsExecute Permission = iota
WorkflowsStepcompleted Permission = iota
WorkflowsStepfailed Permission = iota
WorkflowsUpdatestep Permission = iota
TriggersWrite Permission = iota
WorkflowsTriggersPermissionsAdd Permission = iota
WorkflowsTriggersPermissionsRemove Permission = iota
WorkflowsTriggersPermissionsSet Permission = iota
TriggersRead Permission = iota
WorkflowsTriggersPermissionsList Permission = iota
)
var (
PermissionStrings = map[Permission]string{
AdminAnalyticsRead: "admin.analytics:read",
AdminAnalyticsGetfile: "admin.analytics.getFile",
AdminAppActivitiesRead: "admin.app_activities:read",
AdminAppsActivitiesList: "admin.apps.activities.list",
AdminAppsWrite: "admin.apps:write",
AdminAppsApprove: "admin.apps.approve",
AdminAppsClearresolution: "admin.apps.clearResolution",
AdminAppsConfigSet: "admin.apps.config.set",
AdminAppsRequestsCancel: "admin.apps.requests.cancel",
AdminAppsRestrict: "admin.apps.restrict",
AdminAppsUninstall: "admin.apps.uninstall",
AdminAppsRead: "admin.apps:read",
AdminAppsApprovedList: "admin.apps.approved.list",
AdminAppsConfigLookup: "admin.apps.config.lookup",
AdminAppsRequestsList: "admin.apps.requests.list",
AdminAppsRestrictedList: "admin.apps.restricted.list",
AdminUsersWrite: "admin.users:write",
AdminAuthPolicyAssignentities: "admin.auth.policy.assignEntities",
AdminAuthPolicyRemoveentities: "admin.auth.policy.removeEntities",
AdminUsersAssign: "admin.users.assign",
AdminUsersInvite: "admin.users.invite",
AdminUsersRemove: "admin.users.remove",
AdminUsersSessionClearsettings: "admin.users.session.clearSettings",
AdminUsersSessionInvalidate: "admin.users.session.invalidate",
AdminUsersSessionReset: "admin.users.session.reset",
AdminUsersSessionResetbulk: "admin.users.session.resetBulk",
AdminUsersSessionSetsettings: "admin.users.session.setSettings",
AdminUsersSetadmin: "admin.users.setAdmin",
AdminUsersSetexpiration: "admin.users.setExpiration",
AdminUsersSetowner: "admin.users.setOwner",
AdminUsersSetregular: "admin.users.setRegular",
AdminUsersRead: "admin.users:read",
AdminAuthPolicyGetentities: "admin.auth.policy.getEntities",
AdminUsersList: "admin.users.list",
AdminUsersSessionGetsettings: "admin.users.session.getSettings",
AdminUsersSessionList: "admin.users.session.list",
AdminUsersUnsupportedversionsExport: "admin.users.unsupportedVersions.export",
AdminBarriersWrite: "admin.barriers:write",
AdminBarriersCreate: "admin.barriers.create",
AdminBarriersDelete: "admin.barriers.delete",
AdminBarriersUpdate: "admin.barriers.update",
AdminBarriersRead: "admin.barriers:read",
AdminBarriersList: "admin.barriers.list",
AdminConversationsWrite: "admin.conversations:write",
AdminConversationsArchive: "admin.conversations.archive",
AdminConversationsBulkarchive: "admin.conversations.bulkArchive",
AdminConversationsBulkdelete: "admin.conversations.bulkDelete",
AdminConversationsBulkmove: "admin.conversations.bulkMove",
AdminConversationsConverttoprivate: "admin.conversations.convertToPrivate",
AdminConversationsConverttopublic: "admin.conversations.convertToPublic",
AdminConversationsCreate: "admin.conversations.create",
AdminConversationsDelete: "admin.conversations.delete",
AdminConversationsDisconnectshared: "admin.conversations.disconnectShared",
AdminConversationsInvite: "admin.conversations.invite",
AdminConversationsRemovecustomretention: "admin.conversations.removeCustomRetention",
AdminConversationsRename: "admin.conversations.rename",
AdminConversationsRestrictaccessAddgroup: "admin.conversations.restrictAccess.addGroup",
AdminConversationsRestrictaccessRemovegroup: "admin.conversations.restrictAccess.removeGroup",
AdminConversationsSetconversationprefs: "admin.conversations.setConversationPrefs",
AdminConversationsSetcustomretention: "admin.conversations.setCustomRetention",
AdminConversationsSetteams: "admin.conversations.setTeams",
AdminConversationsUnarchive: "admin.conversations.unarchive",
AdminConversationsRead: "admin.conversations:read",
AdminConversationsEkmListoriginalconnectedchannelinfo: "admin.conversations.ekm.listOriginalConnectedChannelInfo",
AdminConversationsGetconversationprefs: "admin.conversations.getConversationPrefs",
AdminConversationsGetcustomretention: "admin.conversations.getCustomRetention",
AdminConversationsGetteams: "admin.conversations.getTeams",
AdminConversationsLookup: "admin.conversations.lookup",
AdminConversationsRestrictaccessListgroups: "admin.conversations.restrictAccess.listGroups",
AdminConversationsSearch: "admin.conversations.search",
AdminTeamsWrite: "admin.teams:write",
AdminEmojiAdd: "admin.emoji.add",
AdminEmojiAddalias: "admin.emoji.addAlias",
AdminEmojiRemove: "admin.emoji.remove",
AdminTeamsCreate: "admin.teams.create",
AdminTeamsSettingsSetdefaultchannels: "admin.teams.settings.setDefaultChannels",
AdminTeamsSettingsSetdescription: "admin.teams.settings.setDescription",
AdminTeamsSettingsSetdiscoverability: "admin.teams.settings.setDiscoverability",
AdminTeamsSettingsSeticon: "admin.teams.settings.setIcon",
AdminTeamsSettingsSetname: "admin.teams.settings.setName",
AdminUsergroupsAddteams: "admin.usergroups.addTeams",
AdminTeamsRead: "admin.teams:read",
AdminEmojiList: "admin.emoji.list",
AdminTeamsAdminsList: "admin.teams.admins.list",
AdminTeamsList: "admin.teams.list",
AdminTeamsOwnersList: "admin.teams.owners.list",
AdminTeamsSettingsInfo: "admin.teams.settings.info",
AdminWorkflowsRead: "admin.workflows:read",
AdminFunctionsList: "admin.functions.list",
AdminFunctionsPermissionsLookup: "admin.functions.permissions.lookup",
AdminWorkflowsPermissionsLookup: "admin.workflows.permissions.lookup",
AdminWorkflowsSearch: "admin.workflows.search",
AdminWorkflowsWrite: "admin.workflows:write",
AdminFunctionsPermissionsSet: "admin.functions.permissions.set",
AdminWorkflowsCollaboratorsAdd: "admin.workflows.collaborators.add",
AdminWorkflowsCollaboratorsRemove: "admin.workflows.collaborators.remove",
AdminWorkflowsUnpublish: "admin.workflows.unpublish",
AdminInvitesWrite: "admin.invites:write",
AdminInviterequestsApprove: "admin.inviteRequests.approve",
AdminInviterequestsDeny: "admin.inviteRequests.deny",
AdminInvitesRead: "admin.invites:read",
AdminInviterequestsApprovedList: "admin.inviteRequests.approved.list",
AdminInviterequestsDeniedList: "admin.inviteRequests.denied.list",
AdminInviterequestsList: "admin.inviteRequests.list",
AdminRolesWrite: "admin.roles:write",
AdminRolesAddassignments: "admin.roles.addAssignments",
AdminRolesRemoveassignments: "admin.roles.removeAssignments",
AdminRolesRead: "admin.roles:read",
AdminRolesListassignments: "admin.roles.listAssignments",
AdminUsergroupsWrite: "admin.usergroups:write",
AdminUsergroupsAddchannels: "admin.usergroups.addChannels",
AdminUsergroupsRemovechannels: "admin.usergroups.removeChannels",
AdminUsergroupsRead: "admin.usergroups:read",
AdminUsergroupsListchannels: "admin.usergroups.listChannels",
HostingRead: "hosting:read",
AppsActivitiesList: "apps.activities.list",
ConnectionsWrite: "connections:write",
AppsConnectionsOpen: "apps.connections.open",
Token: "token",
AppsDatastoreBulkdelete: "apps.datastore.bulkDelete",
AppsDatastoreBulkget: "apps.datastore.bulkGet",
AppsDatastoreBulkput: "apps.datastore.bulkPut",
AppsDatastoreDelete: "apps.datastore.delete",
AppsDatastoreGet: "apps.datastore.get",
AppsDatastorePut: "apps.datastore.put",
AppsDatastoreQuery: "apps.datastore.query",
AppsDatastoreUpdate: "apps.datastore.update",
DatastoreRead: "datastore:read",
AppsDatastoreCount: "apps.datastore.count",
AuthorizationsRead: "authorizations:read",
AppsEventAuthorizationsList: "apps.event.authorizations.list",
Bot: "bot",
AuthRevoke: "auth.revoke",
AuthTest: "auth.test",
ChatGetpermalink: "chat.getPermalink",
ChatScheduledmessagesList: "chat.scheduledMessages.list",
DialogOpen: "dialog.open",
FunctionsCompleteerror: "functions.completeError",
FunctionsCompletesuccess: "functions.completeSuccess",
RtmConnect: "rtm.connect",
RtmStart: "rtm.start",
ViewsOpen: "views.open",
ViewsPublish: "views.publish",
ViewsPush: "views.push",
ViewsUpdate: "views.update",
BookmarksWrite: "bookmarks:write",
BookmarksAdd: "bookmarks.add",
BookmarksEdit: "bookmarks.edit",
BookmarksRemove: "bookmarks.remove",
BookmarksRead: "bookmarks:read",
BookmarksList: "bookmarks.list",
UsersRead: "users:read",
BotsInfo: "bots.info",
UsersGetpresence: "users.getPresence",
UsersInfo: "users.info",
UsersList: "users.list",
CallsWrite: "calls:write",
CallsAdd: "calls.add",
CallsEnd: "calls.end",
CallsParticipantsAdd: "calls.participants.add",
CallsParticipantsRemove: "calls.participants.remove",
CallsUpdate: "calls.update",
CallsRead: "calls:read",
CallsInfo: "calls.info",
ChannelsManage: "channels:manage",
ChannelsCreate: "channels.create",
ChannelsMark: "channels.mark",
ConversationsArchive: "conversations.archive",
ConversationsClose: "conversations.close",
ConversationsCreate: "conversations.create",
ConversationsKick: "conversations.kick",
ConversationsLeave: "conversations.leave",
ConversationsMark: "conversations.mark",
ConversationsOpen: "conversations.open",
ConversationsRename: "conversations.rename",
ConversationsUnarchive: "conversations.unarchive",
GroupsCreate: "groups.create",
GroupsMark: "groups.mark",
ImMark: "im.mark",
ImOpen: "im.open",
MpimMark: "mpim.mark",
MpimOpen: "mpim.open",
ChannelsRead: "channels:read",
ChannelsInfo: "channels.info",
ConversationsInfo: "conversations.info",
ConversationsList: "conversations.list",
ConversationsMembers: "conversations.members",
GroupsInfo: "groups.info",
ImList: "im.list",
MpimList: "mpim.list",
UsersConversations: "users.conversations",
ChannelsWriteInvites: "channels:write.invites",
ChannelsInvite: "channels.invite",
ConversationsInvite: "conversations.invite",
GroupsInvite: "groups.invite",
ChatWrite: "chat:write",
ChatDelete: "chat.delete",
ChatDeletescheduledmessage: "chat.deleteScheduledMessage",
ChatMemessage: "chat.meMessage",
ChatPostephemeral: "chat.postEphemeral",
ChatPostmessage: "chat.postMessage",
ChatSchedulemessage: "chat.scheduleMessage",
ChatUpdate: "chat.update",
LinksWrite: "links:write",
ChatUnfurl: "chat.unfurl",
ConversationsConnectWrite: "conversations.connect:write",
ConversationsAcceptsharedinvite: "conversations.acceptSharedInvite",
ConversationsInviteshared: "conversations.inviteShared",
ConversationsConnectManage: "conversations.connect:manage",
ConversationsApprovesharedinvite: "conversations.approveSharedInvite",
ConversationsDeclinesharedinvite: "conversations.declineSharedInvite",
ConversationsListconnectinvites: "conversations.listConnectInvites",
ChannelsHistory: "channels:history",
ConversationsHistory: "conversations.history",
ConversationsReplies: "conversations.replies",
ChannelsJoin: "channels:join",
ConversationsJoin: "conversations.join",
ChannelsWriteTopic: "channels:write.topic",
ConversationsSetpurpose: "conversations.setPurpose",
ConversationsSettopic: "conversations.setTopic",
DndWrite: "dnd:write",
DndEnddnd: "dnd.endDnd",
DndEndsnooze: "dnd.endSnooze",
DndSetsnooze: "dnd.setSnooze",
DndRead: "dnd:read",
DndInfo: "dnd.info",
DndTeaminfo: "dnd.teamInfo",
EmojiRead: "emoji:read",
EmojiList: "emoji.list",
FilesWrite: "files:write",
FilesCommentsDelete: "files.comments.delete",
FilesCompleteuploadexternal: "files.completeUploadExternal",
FilesDelete: "files.delete",
FilesGetuploadurlexternal: "files.getUploadURLExternal",
FilesRevokepublicurl: "files.revokePublicURL",
FilesSharedpublicurl: "files.sharedPublicURL",
FilesUpload: "files.upload",
FilesRead: "files:read",
FilesInfo: "files.info",
FilesList: "files.list",
RemoteFilesWrite: "remote_files:write",
FilesRemoteAdd: "files.remote.add",
FilesRemoteRemove: "files.remote.remove",
FilesRemoteUpdate: "files.remote.update",
RemoteFilesRead: "remote_files:read",
FilesRemoteInfo: "files.remote.info",
FilesRemoteList: "files.remote.list",
RemoteFilesShare: "remote_files:share",
FilesRemoteShare: "files.remote.share",
AppConfigurationsWrite: "app_configurations:write",
FunctionsDistributionsPermissionsAdd: "functions.distributions.permissions.add",
FunctionsDistributionsPermissionsRemove: "functions.distributions.permissions.remove",
FunctionsDistributionsPermissionsSet: "functions.distributions.permissions.set",
AppConfigurationsRead: "app_configurations:read",
FunctionsDistributionsPermissionsList: "functions.distributions.permissions.list",
Conversations: "conversations",
GroupsOpen: "groups.open",
TokensBasic: "tokens.basic",
MigrationExchange: "migration.exchange",
Email: "email",
OpenidConnectUserinfo: "openid.connect.userInfo",
PinsWrite: "pins:write",
PinsAdd: "pins.add",
PinsRemove: "pins.remove",
PinsRead: "pins:read",
PinsList: "pins.list",
ReactionsWrite: "reactions:write",
ReactionsAdd: "reactions.add",
ReactionsRemove: "reactions.remove",
ReactionsRead: "reactions:read",
ReactionsGet: "reactions.get",
ReactionsList: "reactions.list",
RemindersWrite: "reminders:write",
RemindersAdd: "reminders.add",
RemindersComplete: "reminders.complete",
RemindersDelete: "reminders.delete",
RemindersRead: "reminders:read",
RemindersInfo: "reminders.info",
RemindersList: "reminders.list",
SearchRead: "search:read",
SearchAll: "search.all",
SearchFiles: "search.files",
SearchMessages: "search.messages",
StarsWrite: "stars:write",
StarsAdd: "stars.add",
StarsRemove: "stars.remove",
StarsRead: "stars:read",
StarsList: "stars.list",
Admin: "admin",
TeamAccesslogs: "team.accessLogs",
TeamBillableinfo: "team.billableInfo",
TeamIntegrationlogs: "team.integrationLogs",
TeamBillingRead: "team.billing:read",
TeamBillingInfo: "team.billing.info",
TeamRead: "team:read",
TeamInfo: "team.info",
TeamPreferencesRead: "team.preferences:read",
TeamPreferencesList: "team.preferences.list",
UsersProfileRead: "users.profile:read",
TeamProfileGet: "team.profile.get",
UsersProfileGet: "users.profile.get",
UsergroupsWrite: "usergroups:write",
UsergroupsCreate: "usergroups.create",
UsergroupsDisable: "usergroups.disable",
UsergroupsEnable: "usergroups.enable",
UsergroupsUpdate: "usergroups.update",
UsergroupsUsersUpdate: "usergroups.users.update",
UsergroupsRead: "usergroups:read",
UsergroupsList: "usergroups.list",
UsergroupsUsersList: "usergroups.users.list",
UsersProfileWrite: "users.profile:write",
UsersDeletephoto: "users.deletePhoto",
UsersProfileSet: "users.profile.set",
UsersSetphoto: "users.setPhoto",
IdentityBasic: "identity.basic",
UsersIdentity: "users.identity",
UsersReadEmail: "users:read.email",
UsersLookupbyemail: "users.lookupByEmail",
UsersWrite: "users:write",
UsersSetactive: "users.setActive",
UsersSetpresence: "users.setPresence",
WorkflowStepsExecute: "workflow.steps:execute",
WorkflowsStepcompleted: "workflows.stepCompleted",
WorkflowsStepfailed: "workflows.stepFailed",
WorkflowsUpdatestep: "workflows.updateStep",
TriggersWrite: "triggers:write",
WorkflowsTriggersPermissionsAdd: "workflows.triggers.permissions.add",
WorkflowsTriggersPermissionsRemove: "workflows.triggers.permissions.remove",
WorkflowsTriggersPermissionsSet: "workflows.triggers.permissions.set",
TriggersRead: "triggers:read",
WorkflowsTriggersPermissionsList: "workflows.triggers.permissions.list",
}
StringToPermission = map[string]Permission{
"admin.analytics:read": AdminAnalyticsRead,
"admin.analytics.getFile": AdminAnalyticsGetfile,
"admin.app_activities:read": AdminAppActivitiesRead,
"admin.apps.activities.list": AdminAppsActivitiesList,
"admin.apps:write": AdminAppsWrite,
"admin.apps.approve": AdminAppsApprove,
"admin.apps.clearResolution": AdminAppsClearresolution,
"admin.apps.config.set": AdminAppsConfigSet,
"admin.apps.requests.cancel": AdminAppsRequestsCancel,
"admin.apps.restrict": AdminAppsRestrict,
"admin.apps.uninstall": AdminAppsUninstall,
"admin.apps:read": AdminAppsRead,
"admin.apps.approved.list": AdminAppsApprovedList,
"admin.apps.config.lookup": AdminAppsConfigLookup,
"admin.apps.requests.list": AdminAppsRequestsList,
"admin.apps.restricted.list": AdminAppsRestrictedList,
"admin.users:write": AdminUsersWrite,
"admin.auth.policy.assignEntities": AdminAuthPolicyAssignentities,
"admin.auth.policy.removeEntities": AdminAuthPolicyRemoveentities,
"admin.users.assign": AdminUsersAssign,
"admin.users.invite": AdminUsersInvite,
"admin.users.remove": AdminUsersRemove,
"admin.users.session.clearSettings": AdminUsersSessionClearsettings,
"admin.users.session.invalidate": AdminUsersSessionInvalidate,
"admin.users.session.reset": AdminUsersSessionReset,
"admin.users.session.resetBulk": AdminUsersSessionResetbulk,
"admin.users.session.setSettings": AdminUsersSessionSetsettings,
"admin.users.setAdmin": AdminUsersSetadmin,
"admin.users.setExpiration": AdminUsersSetexpiration,
"admin.users.setOwner": AdminUsersSetowner,
"admin.users.setRegular": AdminUsersSetregular,
"admin.users:read": AdminUsersRead,
"admin.auth.policy.getEntities": AdminAuthPolicyGetentities,
"admin.users.list": AdminUsersList,
"admin.users.session.getSettings": AdminUsersSessionGetsettings,
"admin.users.session.list": AdminUsersSessionList,
"admin.users.unsupportedVersions.export": AdminUsersUnsupportedversionsExport,
"admin.barriers:write": AdminBarriersWrite,
"admin.barriers.create": AdminBarriersCreate,
"admin.barriers.delete": AdminBarriersDelete,
"admin.barriers.update": AdminBarriersUpdate,
"admin.barriers:read": AdminBarriersRead,
"admin.barriers.list": AdminBarriersList,
"admin.conversations:write": AdminConversationsWrite,
"admin.conversations.archive": AdminConversationsArchive,
"admin.conversations.bulkArchive": AdminConversationsBulkarchive,
"admin.conversations.bulkDelete": AdminConversationsBulkdelete,
"admin.conversations.bulkMove": AdminConversationsBulkmove,
"admin.conversations.convertToPrivate": AdminConversationsConverttoprivate,
"admin.conversations.convertToPublic": AdminConversationsConverttopublic,
"admin.conversations.create": AdminConversationsCreate,
"admin.conversations.delete": AdminConversationsDelete,
"admin.conversations.disconnectShared": AdminConversationsDisconnectshared,
"admin.conversations.invite": AdminConversationsInvite,
"admin.conversations.removeCustomRetention": AdminConversationsRemovecustomretention,
"admin.conversations.rename": AdminConversationsRename,
"admin.conversations.restrictAccess.addGroup": AdminConversationsRestrictaccessAddgroup,
"admin.conversations.restrictAccess.removeGroup": AdminConversationsRestrictaccessRemovegroup,
"admin.conversations.setConversationPrefs": AdminConversationsSetconversationprefs,
"admin.conversations.setCustomRetention": AdminConversationsSetcustomretention,
"admin.conversations.setTeams": AdminConversationsSetteams,
"admin.conversations.unarchive": AdminConversationsUnarchive,
"admin.conversations:read": AdminConversationsRead,
"admin.conversations.ekm.listOriginalConnectedChannelInfo": AdminConversationsEkmListoriginalconnectedchannelinfo,
"admin.conversations.getConversationPrefs": AdminConversationsGetconversationprefs,
"admin.conversations.getCustomRetention": AdminConversationsGetcustomretention,
"admin.conversations.getTeams": AdminConversationsGetteams,
"admin.conversations.lookup": AdminConversationsLookup,
"admin.conversations.restrictAccess.listGroups": AdminConversationsRestrictaccessListgroups,
"admin.conversations.search": AdminConversationsSearch,
"admin.teams:write": AdminTeamsWrite,
"admin.emoji.add": AdminEmojiAdd,
"admin.emoji.addAlias": AdminEmojiAddalias,
"admin.emoji.remove": AdminEmojiRemove,
"admin.teams.create": AdminTeamsCreate,
"admin.teams.settings.setDefaultChannels": AdminTeamsSettingsSetdefaultchannels,
"admin.teams.settings.setDescription": AdminTeamsSettingsSetdescription,
"admin.teams.settings.setDiscoverability": AdminTeamsSettingsSetdiscoverability,
"admin.teams.settings.setIcon": AdminTeamsSettingsSeticon,
"admin.teams.settings.setName": AdminTeamsSettingsSetname,
"admin.usergroups.addTeams": AdminUsergroupsAddteams,
"admin.teams:read": AdminTeamsRead,
"admin.emoji.list": AdminEmojiList,
"admin.teams.admins.list": AdminTeamsAdminsList,
"admin.teams.list": AdminTeamsList,
"admin.teams.owners.list": AdminTeamsOwnersList,
"admin.teams.settings.info": AdminTeamsSettingsInfo,
"admin.workflows:read": AdminWorkflowsRead,
"admin.functions.list": AdminFunctionsList,
"admin.functions.permissions.lookup": AdminFunctionsPermissionsLookup,
"admin.workflows.permissions.lookup": AdminWorkflowsPermissionsLookup,
"admin.workflows.search": AdminWorkflowsSearch,
"admin.workflows:write": AdminWorkflowsWrite,
"admin.functions.permissions.set": AdminFunctionsPermissionsSet,
"admin.workflows.collaborators.add": AdminWorkflowsCollaboratorsAdd,
"admin.workflows.collaborators.remove": AdminWorkflowsCollaboratorsRemove,
"admin.workflows.unpublish": AdminWorkflowsUnpublish,
"admin.invites:write": AdminInvitesWrite,
"admin.inviteRequests.approve": AdminInviterequestsApprove,
"admin.inviteRequests.deny": AdminInviterequestsDeny,
"admin.invites:read": AdminInvitesRead,
"admin.inviteRequests.approved.list": AdminInviterequestsApprovedList,
"admin.inviteRequests.denied.list": AdminInviterequestsDeniedList,
"admin.inviteRequests.list": AdminInviterequestsList,
"admin.roles:write": AdminRolesWrite,
"admin.roles.addAssignments": AdminRolesAddassignments,
"admin.roles.removeAssignments": AdminRolesRemoveassignments,
"admin.roles:read": AdminRolesRead,
"admin.roles.listAssignments": AdminRolesListassignments,
"admin.usergroups:write": AdminUsergroupsWrite,
"admin.usergroups.addChannels": AdminUsergroupsAddchannels,
"admin.usergroups.removeChannels": AdminUsergroupsRemovechannels,
"admin.usergroups:read": AdminUsergroupsRead,
"admin.usergroups.listChannels": AdminUsergroupsListchannels,
"hosting:read": HostingRead,
"apps.activities.list": AppsActivitiesList,
"connections:write": ConnectionsWrite,
"apps.connections.open": AppsConnectionsOpen,
"token": Token,
"apps.datastore.bulkDelete": AppsDatastoreBulkdelete,
"apps.datastore.bulkGet": AppsDatastoreBulkget,
"apps.datastore.bulkPut": AppsDatastoreBulkput,
"apps.datastore.delete": AppsDatastoreDelete,
"apps.datastore.get": AppsDatastoreGet,
"apps.datastore.put": AppsDatastorePut,
"apps.datastore.query": AppsDatastoreQuery,
"apps.datastore.update": AppsDatastoreUpdate,
"datastore:read": DatastoreRead,
"apps.datastore.count": AppsDatastoreCount,
"authorizations:read": AuthorizationsRead,
"apps.event.authorizations.list": AppsEventAuthorizationsList,
"bot": Bot,
"auth.revoke": AuthRevoke,
"auth.test": AuthTest,
"chat.getPermalink": ChatGetpermalink,
"chat.scheduledMessages.list": ChatScheduledmessagesList,
"dialog.open": DialogOpen,
"functions.completeError": FunctionsCompleteerror,
"functions.completeSuccess": FunctionsCompletesuccess,
"rtm.connect": RtmConnect,
"rtm.start": RtmStart,
"views.open": ViewsOpen,
"views.publish": ViewsPublish,
"views.push": ViewsPush,
"views.update": ViewsUpdate,
"bookmarks:write": BookmarksWrite,
"bookmarks.add": BookmarksAdd,
"bookmarks.edit": BookmarksEdit,
"bookmarks.remove": BookmarksRemove,
"bookmarks:read": BookmarksRead,
"bookmarks.list": BookmarksList,
"users:read": UsersRead,
"bots.info": BotsInfo,
"users.getPresence": UsersGetpresence,
"users.info": UsersInfo,
"users.list": UsersList,
"calls:write": CallsWrite,
"calls.add": CallsAdd,
"calls.end": CallsEnd,
"calls.participants.add": CallsParticipantsAdd,
"calls.participants.remove": CallsParticipantsRemove,
"calls.update": CallsUpdate,
"calls:read": CallsRead,
"calls.info": CallsInfo,
"channels:manage": ChannelsManage,
"channels.create": ChannelsCreate,
"channels.mark": ChannelsMark,
"conversations.archive": ConversationsArchive,
"conversations.close": ConversationsClose,
"conversations.create": ConversationsCreate,
"conversations.kick": ConversationsKick,
"conversations.leave": ConversationsLeave,
"conversations.mark": ConversationsMark,
"conversations.open": ConversationsOpen,
"conversations.rename": ConversationsRename,
"conversations.unarchive": ConversationsUnarchive,
"groups.create": GroupsCreate,
"groups.mark": GroupsMark,
"im.mark": ImMark,
"im.open": ImOpen,
"mpim.mark": MpimMark,
"mpim.open": MpimOpen,
"channels:read": ChannelsRead,
"channels.info": ChannelsInfo,
"conversations.info": ConversationsInfo,
"conversations.list": ConversationsList,
"conversations.members": ConversationsMembers,
"groups.info": GroupsInfo,
"im.list": ImList,
"mpim.list": MpimList,
"users.conversations": UsersConversations,
"channels:write.invites": ChannelsWriteInvites,
"channels.invite": ChannelsInvite,
"conversations.invite": ConversationsInvite,
"groups.invite": GroupsInvite,
"chat:write": ChatWrite,
"chat.delete": ChatDelete,
"chat.deleteScheduledMessage": ChatDeletescheduledmessage,
"chat.meMessage": ChatMemessage,
"chat.postEphemeral": ChatPostephemeral,
"chat.postMessage": ChatPostmessage,
"chat.scheduleMessage": ChatSchedulemessage,
"chat.update": ChatUpdate,
"links:write": LinksWrite,
"chat.unfurl": ChatUnfurl,
"conversations.connect:write": ConversationsConnectWrite,
"conversations.acceptSharedInvite": ConversationsAcceptsharedinvite,
"conversations.inviteShared": ConversationsInviteshared,
"conversations.connect:manage": ConversationsConnectManage,
"conversations.approveSharedInvite": ConversationsApprovesharedinvite,
"conversations.declineSharedInvite": ConversationsDeclinesharedinvite,
"conversations.listConnectInvites": ConversationsListconnectinvites,
"channels:history": ChannelsHistory,
"conversations.history": ConversationsHistory,
"conversations.replies": ConversationsReplies,
"channels:join": ChannelsJoin,
"conversations.join": ConversationsJoin,
"channels:write.topic": ChannelsWriteTopic,
"conversations.setPurpose": ConversationsSetpurpose,
"conversations.setTopic": ConversationsSettopic,
"dnd:write": DndWrite,
"dnd.endDnd": DndEnddnd,
"dnd.endSnooze": DndEndsnooze,
"dnd.setSnooze": DndSetsnooze,
"dnd:read": DndRead,
"dnd.info": DndInfo,
"dnd.teamInfo": DndTeaminfo,
"emoji:read": EmojiRead,
"emoji.list": EmojiList,
"files:write": FilesWrite,
"files.comments.delete": FilesCommentsDelete,
"files.completeUploadExternal": FilesCompleteuploadexternal,
"files.delete": FilesDelete,
"files.getUploadURLExternal": FilesGetuploadurlexternal,
"files.revokePublicURL": FilesRevokepublicurl,
"files.sharedPublicURL": FilesSharedpublicurl,
"files.upload": FilesUpload,
"files:read": FilesRead,
"files.info": FilesInfo,
"files.list": FilesList,
"remote_files:write": RemoteFilesWrite,
"files.remote.add": FilesRemoteAdd,
"files.remote.remove": FilesRemoteRemove,
"files.remote.update": FilesRemoteUpdate,
"remote_files:read": RemoteFilesRead,
"files.remote.info": FilesRemoteInfo,
"files.remote.list": FilesRemoteList,
"remote_files:share": RemoteFilesShare,
"files.remote.share": FilesRemoteShare,
"app_configurations:write": AppConfigurationsWrite,
"functions.distributions.permissions.add": FunctionsDistributionsPermissionsAdd,
"functions.distributions.permissions.remove": FunctionsDistributionsPermissionsRemove,
"functions.distributions.permissions.set": FunctionsDistributionsPermissionsSet,
"app_configurations:read": AppConfigurationsRead,
"functions.distributions.permissions.list": FunctionsDistributionsPermissionsList,
"conversations": Conversations,
"groups.open": GroupsOpen,
"tokens.basic": TokensBasic,
"migration.exchange": MigrationExchange,
"email": Email,
"openid.connect.userInfo": OpenidConnectUserinfo,
"pins:write": PinsWrite,
"pins.add": PinsAdd,
"pins.remove": PinsRemove,
"pins:read": PinsRead,
"pins.list": PinsList,
"reactions:write": ReactionsWrite,
"reactions.add": ReactionsAdd,
"reactions.remove": ReactionsRemove,
"reactions:read": ReactionsRead,
"reactions.get": ReactionsGet,
"reactions.list": ReactionsList,
"reminders:write": RemindersWrite,
"reminders.add": RemindersAdd,
"reminders.complete": RemindersComplete,
"reminders.delete": RemindersDelete,
"reminders:read": RemindersRead,
"reminders.info": RemindersInfo,
"reminders.list": RemindersList,
"search:read": SearchRead,
"search.all": SearchAll,
"search.files": SearchFiles,
"search.messages": SearchMessages,
"stars:write": StarsWrite,
"stars.add": StarsAdd,
"stars.remove": StarsRemove,
"stars:read": StarsRead,
"stars.list": StarsList,
"admin": Admin,
"team.accessLogs": TeamAccesslogs,
"team.billableInfo": TeamBillableinfo,
"team.integrationLogs": TeamIntegrationlogs,
"team.billing:read": TeamBillingRead,
"team.billing.info": TeamBillingInfo,
"team:read": TeamRead,
"team.info": TeamInfo,
"team.preferences:read": TeamPreferencesRead,
"team.preferences.list": TeamPreferencesList,
"users.profile:read": UsersProfileRead,
"team.profile.get": TeamProfileGet,
"users.profile.get": UsersProfileGet,
"usergroups:write": UsergroupsWrite,
"usergroups.create": UsergroupsCreate,
"usergroups.disable": UsergroupsDisable,
"usergroups.enable": UsergroupsEnable,
"usergroups.update": UsergroupsUpdate,
"usergroups.users.update": UsergroupsUsersUpdate,
"usergroups:read": UsergroupsRead,
"usergroups.list": UsergroupsList,
"usergroups.users.list": UsergroupsUsersList,
"users.profile:write": UsersProfileWrite,
"users.deletePhoto": UsersDeletephoto,
"users.profile.set": UsersProfileSet,
"users.setPhoto": UsersSetphoto,
"identity.basic": IdentityBasic,
"users.identity": UsersIdentity,
"users:read.email": UsersReadEmail,
"users.lookupByEmail": UsersLookupbyemail,
"users:write": UsersWrite,
"users.setActive": UsersSetactive,
"users.setPresence": UsersSetpresence,
"workflow.steps:execute": WorkflowStepsExecute,
"workflows.stepCompleted": WorkflowsStepcompleted,
"workflows.stepFailed": WorkflowsStepfailed,
"workflows.updateStep": WorkflowsUpdatestep,
"triggers:write": TriggersWrite,
"workflows.triggers.permissions.add": WorkflowsTriggersPermissionsAdd,
"workflows.triggers.permissions.remove": WorkflowsTriggersPermissionsRemove,
"workflows.triggers.permissions.set": WorkflowsTriggersPermissionsSet,
"triggers:read": TriggersRead,
"workflows.triggers.permissions.list": WorkflowsTriggersPermissionsList,
}
PermissionIDs = map[Permission]int{
AdminAnalyticsRead: 1,
AdminAnalyticsGetfile: 2,
AdminAppActivitiesRead: 3,
AdminAppsActivitiesList: 4,
AdminAppsWrite: 5,
AdminAppsApprove: 6,
AdminAppsClearresolution: 7,
AdminAppsConfigSet: 8,
AdminAppsRequestsCancel: 9,
AdminAppsRestrict: 10,
AdminAppsUninstall: 11,
AdminAppsRead: 12,
AdminAppsApprovedList: 13,
AdminAppsConfigLookup: 14,
AdminAppsRequestsList: 15,
AdminAppsRestrictedList: 16,
AdminUsersWrite: 17,
AdminAuthPolicyAssignentities: 18,
AdminAuthPolicyRemoveentities: 19,
AdminUsersAssign: 20,
AdminUsersInvite: 21,
AdminUsersRemove: 22,
AdminUsersSessionClearsettings: 23,
AdminUsersSessionInvalidate: 24,
AdminUsersSessionReset: 25,
AdminUsersSessionResetbulk: 26,
AdminUsersSessionSetsettings: 27,
AdminUsersSetadmin: 28,
AdminUsersSetexpiration: 29,
AdminUsersSetowner: 30,
AdminUsersSetregular: 31,
AdminUsersRead: 32,
AdminAuthPolicyGetentities: 33,
AdminUsersList: 34,
AdminUsersSessionGetsettings: 35,
AdminUsersSessionList: 36,
AdminUsersUnsupportedversionsExport: 37,
AdminBarriersWrite: 38,
AdminBarriersCreate: 39,
AdminBarriersDelete: 40,
AdminBarriersUpdate: 41,
AdminBarriersRead: 42,
AdminBarriersList: 43,
AdminConversationsWrite: 44,
AdminConversationsArchive: 45,
AdminConversationsBulkarchive: 46,
AdminConversationsBulkdelete: 47,
AdminConversationsBulkmove: 48,
AdminConversationsConverttoprivate: 49,
AdminConversationsConverttopublic: 50,
AdminConversationsCreate: 51,
AdminConversationsDelete: 52,
AdminConversationsDisconnectshared: 53,
AdminConversationsInvite: 54,
AdminConversationsRemovecustomretention: 55,
AdminConversationsRename: 56,
AdminConversationsRestrictaccessAddgroup: 57,
AdminConversationsRestrictaccessRemovegroup: 58,
AdminConversationsSetconversationprefs: 59,
AdminConversationsSetcustomretention: 60,
AdminConversationsSetteams: 61,
AdminConversationsUnarchive: 62,
AdminConversationsRead: 63,
AdminConversationsEkmListoriginalconnectedchannelinfo: 64,
AdminConversationsGetconversationprefs: 65,
AdminConversationsGetcustomretention: 66,
AdminConversationsGetteams: 67,
AdminConversationsLookup: 68,
AdminConversationsRestrictaccessListgroups: 69,
AdminConversationsSearch: 70,
AdminTeamsWrite: 71,
AdminEmojiAdd: 72,
AdminEmojiAddalias: 73,
AdminEmojiRemove: 74,
AdminTeamsCreate: 75,
AdminTeamsSettingsSetdefaultchannels: 76,
AdminTeamsSettingsSetdescription: 77,
AdminTeamsSettingsSetdiscoverability: 78,
AdminTeamsSettingsSeticon: 79,
AdminTeamsSettingsSetname: 80,
AdminUsergroupsAddteams: 81,
AdminTeamsRead: 82,
AdminEmojiList: 83,
AdminTeamsAdminsList: 84,
AdminTeamsList: 85,
AdminTeamsOwnersList: 86,
AdminTeamsSettingsInfo: 87,
AdminWorkflowsRead: 88,
AdminFunctionsList: 89,
AdminFunctionsPermissionsLookup: 90,
AdminWorkflowsPermissionsLookup: 91,
AdminWorkflowsSearch: 92,
AdminWorkflowsWrite: 93,
AdminFunctionsPermissionsSet: 94,
AdminWorkflowsCollaboratorsAdd: 95,
AdminWorkflowsCollaboratorsRemove: 96,
AdminWorkflowsUnpublish: 97,
AdminInvitesWrite: 98,
AdminInviterequestsApprove: 99,
AdminInviterequestsDeny: 100,
AdminInvitesRead: 101,
AdminInviterequestsApprovedList: 102,
AdminInviterequestsDeniedList: 103,
AdminInviterequestsList: 104,
AdminRolesWrite: 105,
AdminRolesAddassignments: 106,
AdminRolesRemoveassignments: 107,
AdminRolesRead: 108,
AdminRolesListassignments: 109,
AdminUsergroupsWrite: 110,
AdminUsergroupsAddchannels: 111,
AdminUsergroupsRemovechannels: 112,
AdminUsergroupsRead: 113,
AdminUsergroupsListchannels: 114,
HostingRead: 115,
AppsActivitiesList: 116,
ConnectionsWrite: 117,
AppsConnectionsOpen: 118,
Token: 119,
AppsDatastoreBulkdelete: 120,
AppsDatastoreBulkget: 121,
AppsDatastoreBulkput: 122,
AppsDatastoreDelete: 123,
AppsDatastoreGet: 124,
AppsDatastorePut: 125,
AppsDatastoreQuery: 126,
AppsDatastoreUpdate: 127,
DatastoreRead: 128,
AppsDatastoreCount: 129,
AuthorizationsRead: 130,
AppsEventAuthorizationsList: 131,
Bot: 132,
AuthRevoke: 133,
AuthTest: 134,
ChatGetpermalink: 135,
ChatScheduledmessagesList: 136,
DialogOpen: 137,
FunctionsCompleteerror: 138,
FunctionsCompletesuccess: 139,
RtmConnect: 140,
RtmStart: 141,
ViewsOpen: 142,
ViewsPublish: 143,
ViewsPush: 144,
ViewsUpdate: 145,
BookmarksWrite: 146,
BookmarksAdd: 147,
BookmarksEdit: 148,
BookmarksRemove: 149,
BookmarksRead: 150,
BookmarksList: 151,
UsersRead: 152,
BotsInfo: 153,
UsersGetpresence: 154,
UsersInfo: 155,
UsersList: 156,
CallsWrite: 157,
CallsAdd: 158,
CallsEnd: 159,
CallsParticipantsAdd: 160,
CallsParticipantsRemove: 161,
CallsUpdate: 162,
CallsRead: 163,
CallsInfo: 164,
ChannelsManage: 165,
ChannelsCreate: 166,
ChannelsMark: 167,
ConversationsArchive: 168,
ConversationsClose: 169,
ConversationsCreate: 170,
ConversationsKick: 171,
ConversationsLeave: 172,
ConversationsMark: 173,
ConversationsOpen: 174,
ConversationsRename: 175,
ConversationsUnarchive: 176,
GroupsCreate: 177,
GroupsMark: 178,
ImMark: 179,
ImOpen: 180,
MpimMark: 181,
MpimOpen: 182,
ChannelsRead: 183,
ChannelsInfo: 184,
ConversationsInfo: 185,
ConversationsList: 186,
ConversationsMembers: 187,
GroupsInfo: 188,
ImList: 189,
MpimList: 190,
UsersConversations: 191,
ChannelsWriteInvites: 192,
ChannelsInvite: 193,
ConversationsInvite: 194,
GroupsInvite: 195,
ChatWrite: 196,
ChatDelete: 197,
ChatDeletescheduledmessage: 198,
ChatMemessage: 199,
ChatPostephemeral: 200,
ChatPostmessage: 201,
ChatSchedulemessage: 202,
ChatUpdate: 203,
LinksWrite: 204,
ChatUnfurl: 205,
ConversationsConnectWrite: 206,
ConversationsAcceptsharedinvite: 207,
ConversationsInviteshared: 208,
ConversationsConnectManage: 209,
ConversationsApprovesharedinvite: 210,
ConversationsDeclinesharedinvite: 211,
ConversationsListconnectinvites: 212,
ChannelsHistory: 213,
ConversationsHistory: 214,
ConversationsReplies: 215,
ChannelsJoin: 216,
ConversationsJoin: 217,
ChannelsWriteTopic: 218,
ConversationsSetpurpose: 219,
ConversationsSettopic: 220,
DndWrite: 221,
DndEnddnd: 222,
DndEndsnooze: 223,
DndSetsnooze: 224,
DndRead: 225,
DndInfo: 226,
DndTeaminfo: 227,
EmojiRead: 228,
EmojiList: 229,
FilesWrite: 230,
FilesCommentsDelete: 231,
FilesCompleteuploadexternal: 232,
FilesDelete: 233,
FilesGetuploadurlexternal: 234,
FilesRevokepublicurl: 235,
FilesSharedpublicurl: 236,
FilesUpload: 237,
FilesRead: 238,
FilesInfo: 239,
FilesList: 240,
RemoteFilesWrite: 241,
FilesRemoteAdd: 242,
FilesRemoteRemove: 243,
FilesRemoteUpdate: 244,
RemoteFilesRead: 245,
FilesRemoteInfo: 246,
FilesRemoteList: 247,
RemoteFilesShare: 248,
FilesRemoteShare: 249,
AppConfigurationsWrite: 250,
FunctionsDistributionsPermissionsAdd: 251,
FunctionsDistributionsPermissionsRemove: 252,
FunctionsDistributionsPermissionsSet: 253,
AppConfigurationsRead: 254,
FunctionsDistributionsPermissionsList: 255,
Conversations: 256,
GroupsOpen: 257,
TokensBasic: 258,
MigrationExchange: 259,
Email: 260,
OpenidConnectUserinfo: 261,
PinsWrite: 262,
PinsAdd: 263,
PinsRemove: 264,
PinsRead: 265,
PinsList: 266,
ReactionsWrite: 267,
ReactionsAdd: 268,
ReactionsRemove: 269,
ReactionsRead: 270,
ReactionsGet: 271,
ReactionsList: 272,
RemindersWrite: 273,
RemindersAdd: 274,
RemindersComplete: 275,
RemindersDelete: 276,
RemindersRead: 277,
RemindersInfo: 278,
RemindersList: 279,
SearchRead: 280,
SearchAll: 281,
SearchFiles: 282,
SearchMessages: 283,
StarsWrite: 284,
StarsAdd: 285,
StarsRemove: 286,
StarsRead: 287,
StarsList: 288,
Admin: 289,
TeamAccesslogs: 290,
TeamBillableinfo: 291,
TeamIntegrationlogs: 292,
TeamBillingRead: 293,
TeamBillingInfo: 294,
TeamRead: 295,
TeamInfo: 296,
TeamPreferencesRead: 297,
TeamPreferencesList: 298,
UsersProfileRead: 299,
TeamProfileGet: 300,
UsersProfileGet: 301,
UsergroupsWrite: 302,
UsergroupsCreate: 303,
UsergroupsDisable: 304,
UsergroupsEnable: 305,
UsergroupsUpdate: 306,
UsergroupsUsersUpdate: 307,
UsergroupsRead: 308,
UsergroupsList: 309,
UsergroupsUsersList: 310,
UsersProfileWrite: 311,
UsersDeletephoto: 312,
UsersProfileSet: 313,
UsersSetphoto: 314,
IdentityBasic: 315,
UsersIdentity: 316,
UsersReadEmail: 317,
UsersLookupbyemail: 318,
UsersWrite: 319,
UsersSetactive: 320,
UsersSetpresence: 321,
WorkflowStepsExecute: 322,
WorkflowsStepcompleted: 323,
WorkflowsStepfailed: 324,
WorkflowsUpdatestep: 325,
TriggersWrite: 326,
WorkflowsTriggersPermissionsAdd: 327,
WorkflowsTriggersPermissionsRemove: 328,
WorkflowsTriggersPermissionsSet: 329,
TriggersRead: 330,
WorkflowsTriggersPermissionsList: 331,
}
IdToPermission = map[int]Permission{
1: AdminAnalyticsRead,
2: AdminAnalyticsGetfile,
3: AdminAppActivitiesRead,
4: AdminAppsActivitiesList,
5: AdminAppsWrite,
6: AdminAppsApprove,
7: AdminAppsClearresolution,
8: AdminAppsConfigSet,
9: AdminAppsRequestsCancel,
10: AdminAppsRestrict,
11: AdminAppsUninstall,
12: AdminAppsRead,
13: AdminAppsApprovedList,
14: AdminAppsConfigLookup,
15: AdminAppsRequestsList,
16: AdminAppsRestrictedList,
17: AdminUsersWrite,
18: AdminAuthPolicyAssignentities,
19: AdminAuthPolicyRemoveentities,
20: AdminUsersAssign,
21: AdminUsersInvite,
22: AdminUsersRemove,
23: AdminUsersSessionClearsettings,
24: AdminUsersSessionInvalidate,
25: AdminUsersSessionReset,
26: AdminUsersSessionResetbulk,
27: AdminUsersSessionSetsettings,
28: AdminUsersSetadmin,
29: AdminUsersSetexpiration,
30: AdminUsersSetowner,
31: AdminUsersSetregular,
32: AdminUsersRead,
33: AdminAuthPolicyGetentities,
34: AdminUsersList,
35: AdminUsersSessionGetsettings,
36: AdminUsersSessionList,
37: AdminUsersUnsupportedversionsExport,
38: AdminBarriersWrite,
39: AdminBarriersCreate,
40: AdminBarriersDelete,
41: AdminBarriersUpdate,
42: AdminBarriersRead,
43: AdminBarriersList,
44: AdminConversationsWrite,
45: AdminConversationsArchive,
46: AdminConversationsBulkarchive,
47: AdminConversationsBulkdelete,
48: AdminConversationsBulkmove,
49: AdminConversationsConverttoprivate,
50: AdminConversationsConverttopublic,
51: AdminConversationsCreate,
52: AdminConversationsDelete,
53: AdminConversationsDisconnectshared,
54: AdminConversationsInvite,
55: AdminConversationsRemovecustomretention,
56: AdminConversationsRename,
57: AdminConversationsRestrictaccessAddgroup,
58: AdminConversationsRestrictaccessRemovegroup,
59: AdminConversationsSetconversationprefs,
60: AdminConversationsSetcustomretention,
61: AdminConversationsSetteams,
62: AdminConversationsUnarchive,
63: AdminConversationsRead,
64: AdminConversationsEkmListoriginalconnectedchannelinfo,
65: AdminConversationsGetconversationprefs,
66: AdminConversationsGetcustomretention,
67: AdminConversationsGetteams,
68: AdminConversationsLookup,
69: AdminConversationsRestrictaccessListgroups,
70: AdminConversationsSearch,
71: AdminTeamsWrite,
72: AdminEmojiAdd,
73: AdminEmojiAddalias,
74: AdminEmojiRemove,
75: AdminTeamsCreate,
76: AdminTeamsSettingsSetdefaultchannels,
77: AdminTeamsSettingsSetdescription,
78: AdminTeamsSettingsSetdiscoverability,
79: AdminTeamsSettingsSeticon,
80: AdminTeamsSettingsSetname,
81: AdminUsergroupsAddteams,
82: AdminTeamsRead,
83: AdminEmojiList,
84: AdminTeamsAdminsList,
85: AdminTeamsList,
86: AdminTeamsOwnersList,
87: AdminTeamsSettingsInfo,
88: AdminWorkflowsRead,
89: AdminFunctionsList,
90: AdminFunctionsPermissionsLookup,
91: AdminWorkflowsPermissionsLookup,
92: AdminWorkflowsSearch,
93: AdminWorkflowsWrite,
94: AdminFunctionsPermissionsSet,
95: AdminWorkflowsCollaboratorsAdd,
96: AdminWorkflowsCollaboratorsRemove,
97: AdminWorkflowsUnpublish,
98: AdminInvitesWrite,
99: AdminInviterequestsApprove,
100: AdminInviterequestsDeny,
101: AdminInvitesRead,
102: AdminInviterequestsApprovedList,
103: AdminInviterequestsDeniedList,
104: AdminInviterequestsList,
105: AdminRolesWrite,
106: AdminRolesAddassignments,
107: AdminRolesRemoveassignments,
108: AdminRolesRead,
109: AdminRolesListassignments,
110: AdminUsergroupsWrite,
111: AdminUsergroupsAddchannels,
112: AdminUsergroupsRemovechannels,
113: AdminUsergroupsRead,
114: AdminUsergroupsListchannels,
115: HostingRead,
116: AppsActivitiesList,
117: ConnectionsWrite,
118: AppsConnectionsOpen,
119: Token,
120: AppsDatastoreBulkdelete,
121: AppsDatastoreBulkget,
122: AppsDatastoreBulkput,
123: AppsDatastoreDelete,
124: AppsDatastoreGet,
125: AppsDatastorePut,
126: AppsDatastoreQuery,
127: AppsDatastoreUpdate,
128: DatastoreRead,
129: AppsDatastoreCount,
130: AuthorizationsRead,
131: AppsEventAuthorizationsList,
132: Bot,
133: AuthRevoke,
134: AuthTest,
135: ChatGetpermalink,
136: ChatScheduledmessagesList,
137: DialogOpen,
138: FunctionsCompleteerror,
139: FunctionsCompletesuccess,
140: RtmConnect,
141: RtmStart,
142: ViewsOpen,
143: ViewsPublish,
144: ViewsPush,
145: ViewsUpdate,
146: BookmarksWrite,
147: BookmarksAdd,
148: BookmarksEdit,
149: BookmarksRemove,
150: BookmarksRead,
151: BookmarksList,
152: UsersRead,
153: BotsInfo,
154: UsersGetpresence,
155: UsersInfo,
156: UsersList,
157: CallsWrite,
158: CallsAdd,
159: CallsEnd,
160: CallsParticipantsAdd,
161: CallsParticipantsRemove,
162: CallsUpdate,
163: CallsRead,
164: CallsInfo,
165: ChannelsManage,
166: ChannelsCreate,
167: ChannelsMark,
168: ConversationsArchive,
169: ConversationsClose,
170: ConversationsCreate,
171: ConversationsKick,
172: ConversationsLeave,
173: ConversationsMark,
174: ConversationsOpen,
175: ConversationsRename,
176: ConversationsUnarchive,
177: GroupsCreate,
178: GroupsMark,
179: ImMark,
180: ImOpen,
181: MpimMark,
182: MpimOpen,
183: ChannelsRead,
184: ChannelsInfo,
185: ConversationsInfo,
186: ConversationsList,
187: ConversationsMembers,
188: GroupsInfo,
189: ImList,
190: MpimList,
191: UsersConversations,
192: ChannelsWriteInvites,
193: ChannelsInvite,
194: ConversationsInvite,
195: GroupsInvite,
196: ChatWrite,
197: ChatDelete,
198: ChatDeletescheduledmessage,
199: ChatMemessage,
200: ChatPostephemeral,
201: ChatPostmessage,
202: ChatSchedulemessage,
203: ChatUpdate,
204: LinksWrite,
205: ChatUnfurl,
206: ConversationsConnectWrite,
207: ConversationsAcceptsharedinvite,
208: ConversationsInviteshared,
209: ConversationsConnectManage,
210: ConversationsApprovesharedinvite,
211: ConversationsDeclinesharedinvite,
212: ConversationsListconnectinvites,
213: ChannelsHistory,
214: ConversationsHistory,
215: ConversationsReplies,
216: ChannelsJoin,
217: ConversationsJoin,
218: ChannelsWriteTopic,
219: ConversationsSetpurpose,
220: ConversationsSettopic,
221: DndWrite,
222: DndEnddnd,
223: DndEndsnooze,
224: DndSetsnooze,
225: DndRead,
226: DndInfo,
227: DndTeaminfo,
228: EmojiRead,
229: EmojiList,
230: FilesWrite,
231: FilesCommentsDelete,
232: FilesCompleteuploadexternal,
233: FilesDelete,
234: FilesGetuploadurlexternal,
235: FilesRevokepublicurl,
236: FilesSharedpublicurl,
237: FilesUpload,
238: FilesRead,
239: FilesInfo,
240: FilesList,
241: RemoteFilesWrite,
242: FilesRemoteAdd,
243: FilesRemoteRemove,
244: FilesRemoteUpdate,
245: RemoteFilesRead,
246: FilesRemoteInfo,
247: FilesRemoteList,
248: RemoteFilesShare,
249: FilesRemoteShare,
250: AppConfigurationsWrite,
251: FunctionsDistributionsPermissionsAdd,
252: FunctionsDistributionsPermissionsRemove,
253: FunctionsDistributionsPermissionsSet,
254: AppConfigurationsRead,
255: FunctionsDistributionsPermissionsList,
256: Conversations,
257: GroupsOpen,
258: TokensBasic,
259: MigrationExchange,
260: Email,
261: OpenidConnectUserinfo,
262: PinsWrite,
263: PinsAdd,
264: PinsRemove,
265: PinsRead,
266: PinsList,
267: ReactionsWrite,
268: ReactionsAdd,
269: ReactionsRemove,
270: ReactionsRead,
271: ReactionsGet,
272: ReactionsList,
273: RemindersWrite,
274: RemindersAdd,
275: RemindersComplete,
276: RemindersDelete,
277: RemindersRead,
278: RemindersInfo,
279: RemindersList,
280: SearchRead,
281: SearchAll,
282: SearchFiles,
283: SearchMessages,
284: StarsWrite,
285: StarsAdd,
286: StarsRemove,
287: StarsRead,
288: StarsList,
289: Admin,
290: TeamAccesslogs,
291: TeamBillableinfo,
292: TeamIntegrationlogs,
293: TeamBillingRead,
294: TeamBillingInfo,
295: TeamRead,
296: TeamInfo,
297: TeamPreferencesRead,
298: TeamPreferencesList,
299: UsersProfileRead,
300: TeamProfileGet,
301: UsersProfileGet,
302: UsergroupsWrite,
303: UsergroupsCreate,
304: UsergroupsDisable,
305: UsergroupsEnable,
306: UsergroupsUpdate,
307: UsergroupsUsersUpdate,
308: UsergroupsRead,
309: UsergroupsList,
310: UsergroupsUsersList,
311: UsersProfileWrite,
312: UsersDeletephoto,
313: UsersProfileSet,
314: UsersSetphoto,
315: IdentityBasic,
316: UsersIdentity,
317: UsersReadEmail,
318: UsersLookupbyemail,
319: UsersWrite,
320: UsersSetactive,
321: UsersSetpresence,
322: WorkflowStepsExecute,
323: WorkflowsStepcompleted,
324: WorkflowsStepfailed,
325: WorkflowsUpdatestep,
326: TriggersWrite,
327: WorkflowsTriggersPermissionsAdd,
328: WorkflowsTriggersPermissionsRemove,
329: WorkflowsTriggersPermissionsSet,
330: TriggersRead,
331: WorkflowsTriggersPermissionsList,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/slack/permissions.yaml
================================================
permissions:
- admin.analytics.getFile
- admin.apps.activities.list
- admin.apps.approve
- admin.apps.clearResolution
- admin.apps.config.set
- admin.apps.requests.cancel
- admin.apps.restrict
- admin.apps.uninstall
- admin.apps.approved.list
- admin.apps.config.lookup
- admin.apps.requests.list
- admin.apps.restricted.list
- admin.auth.policy.assignEntities
- admin.auth.policy.removeEntities
- admin.users.assign
- admin.users.invite
- admin.users.remove
- admin.users.session.clearSettings
- admin.users.session.invalidate
- admin.users.session.reset
- admin.users.session.resetBulk
- admin.users.session.setSettings
- admin.users.setAdmin
- admin.users.setExpiration
- admin.users.setOwner
- admin.users.setRegular
- admin.auth.policy.getEntities
- admin.users.list
- admin.users.session.getSettings
- admin.users.session.list
- admin.users.unsupportedVersions.export
- admin.barriers.create
- admin.barriers.delete
- admin.barriers.update
- admin.barriers.list
- admin.conversations.archive
- admin.conversations.bulkArchive
- admin.conversations.bulkDelete
- admin.conversations.bulkMove
- admin.conversations.convertToPrivate
- admin.conversations.convertToPublic
- admin.conversations.create
- admin.conversations.delete
- admin.conversations.disconnectShared
- admin.conversations.invite
- admin.conversations.removeCustomRetention
- admin.conversations.rename
- admin.conversations.restrictAccess.addGroup
- admin.conversations.restrictAccess.removeGroup
- admin.conversations.setConversationPrefs
- admin.conversations.setCustomRetention
- admin.conversations.setTeams
- admin.conversations.unarchive
- admin.conversations.ekm.listOriginalConnectedChannelInfo
- admin.conversations.getConversationPrefs
- admin.conversations.getCustomRetention
- admin.conversations.getTeams
- admin.conversations.lookup
- admin.conversations.restrictAccess.listGroups
- admin.conversations.search
- admin.emoji.add
- admin.emoji.addAlias
- admin.emoji.remove
- admin.teams.create
- admin.teams.settings.setDefaultChannels
- admin.teams.settings.setDescription
- admin.teams.settings.setDiscoverability
- admin.teams.settings.setIcon
- admin.teams.settings.setName
- admin.usergroups.addTeams
- admin.emoji.list
- admin.teams.admins.list
- admin.teams.list
- admin.teams.owners.list
- admin.teams.settings.info
- admin.functions.list
- admin.functions.permissions.lookup
- admin.workflows.permissions.lookup
- admin.workflows.search
- admin.functions.permissions.set
- admin.workflows.collaborators.add
- admin.workflows.collaborators.remove
- admin.workflows.unpublish
- admin.inviteRequests.approve
- admin.inviteRequests.deny
- admin.inviteRequests.approved.list
- admin.inviteRequests.denied.list
- admin.inviteRequests.list
- admin.roles.addAssignments
- admin.roles.removeAssignments
- admin.roles.listAssignments
- admin.usergroups.addChannels
- admin.usergroups.removeChannels
- admin.usergroups.listChannels
- apps.activities.list
- apps.connections.open
- token
- apps.datastore.bulkDelete
- apps.datastore.bulkGet
- apps.datastore.bulkPut
- apps.datastore.delete
- apps.datastore.get
- apps.datastore.put
- apps.datastore.query
- apps.datastore.update
- apps.datastore.count
- apps.event.authorizations.list
- bot
- auth.revoke
- auth.test
- chat.getPermalink
- chat.scheduledMessages.list
- dialog.open
- functions.completeError
- functions.completeSuccess
- rtm.connect
- rtm.start
- views.open
- views.publish
- views.push
- views.update
- bookmarks.add
- bookmarks.edit
- bookmarks.remove
- bookmarks.list
- bots.info
- users.getPresence
- users.info
- users.list
- calls.add
- calls.end
- calls.participants.add
- calls.participants.remove
- calls.update
- calls.info
- channels.create
- channels.mark
- conversations.archive
- conversations.close
- conversations.create
- conversations.kick
- conversations.leave
- conversations.mark
- conversations.open
- conversations.rename
- conversations.unarchive
- groups.create
- groups.mark
- im.mark
- im.open
- mpim.mark
- mpim.open
- channels.info
- conversations.info
- conversations.list
- conversations.members
- groups.info
- im.list
- mpim.list
- users.conversations
- channels.invite
- conversations.invite
- groups.invite
- chat.delete
- chat.deleteScheduledMessage
- chat.meMessage
- chat.postEphemeral
- chat.postMessage
- chat.scheduleMessage
- chat.update
- chat.unfurl
- conversations.acceptSharedInvite
- conversations.inviteShared
- conversations.approveSharedInvite
- conversations.declineSharedInvite
- conversations.listConnectInvites
- conversations.history
- conversations.replies
- conversations.join
- conversations.setPurpose
- conversations.setTopic
- dnd.endDnd
- dnd.endSnooze
- dnd.setSnooze
- dnd.info
- dnd.teamInfo
- emoji.list
- files.comments.delete
- files.completeUploadExternal
- files.delete
- files.getUploadURLExternal
- files.revokePublicURL
- files.sharedPublicURL
- files.upload
- files.info
- files.list
- files.remote.add
- files.remote.remove
- files.remote.update
- files.remote.info
- files.remote.list
- files.remote.share
- functions.distributions.permissions.add
- functions.distributions.permissions.remove
- functions.distributions.permissions.set
- functions.distributions.permissions.list
- conversations
- groups.open
- tokens.basic
- migration.exchange
- email
- openid.connect.userInfo
- pins.add
- pins.remove
- pins.list
- reactions.add
- reactions.remove
- reactions.get
- reactions.list
- reminders.add
- reminders.complete
- reminders.delete
- reminders.info
- reminders.list
- search.all
- search.files
- search.messages
- stars.add
- stars.remove
- stars.list
- admin
- team.accessLogs
- team.billableInfo
- team.integrationLogs
- team.billing.info
- team.info
- team.preferences.list
- team.profile.get
- users.profile.get
- usergroups.create
- usergroups.disable
- usergroups.enable
- usergroups.update
- usergroups.users.update
- usergroups.list
- usergroups.users.list
- users.deletePhoto
- users.profile.set
- users.setPhoto
- identity.basic
- users.identity
- users.lookupByEmail
- users.setActive
- users.setPresence
- workflows.stepCompleted
- workflows.stepFailed
- workflows.updateStep
- workflows.triggers.permissions.add
- workflows.triggers.permissions.remove
- workflows.triggers.permissions.set
- workflows.triggers.permissions.list
================================================
FILE: pkg/analyzer/analyzers/slack/scopes.go
================================================
package slack
// SCOPES := []string{string} {
// "admin.analytics:read" : {
// "admin.analytics.getFile",
// "admin.analytics.getUsage",
// "admin.analytics.listFiles",
// }
// }
var scope_mapping = map[string][]string{
"admin.analytics:read": {"admin.analytics.getFile"},
"admin.app_activities:read": {"admin.apps.activities.list"},
"admin.apps:write": {"admin.apps.approve", "admin.apps.clearResolution", "admin.apps.config.set", "admin.apps.requests.cancel", "admin.apps.restrict", "admin.apps.uninstall"},
"admin.apps:read": {"admin.apps.approved.list", "admin.apps.config.lookup", "admin.apps.requests.list", "admin.apps.restricted.list"},
"admin.users:write": {"admin.auth.policy.assignEntities", "admin.auth.policy.removeEntities", "admin.users.assign", "admin.users.invite", "admin.users.remove", "admin.users.session.clearSettings", "admin.users.session.invalidate", "admin.users.session.reset", "admin.users.session.resetBulk", "admin.users.session.setSettings", "admin.users.setAdmin", "admin.users.setExpiration", "admin.users.setOwner", "admin.users.setRegular"},
"admin.users:read": {"admin.auth.policy.getEntities", "admin.users.list", "admin.users.session.getSettings", "admin.users.session.list", "admin.users.unsupportedVersions.export"},
"admin.barriers:write": {"admin.barriers.create", "admin.barriers.delete", "admin.barriers.update"},
"admin.barriers:read": {"admin.barriers.list"},
"admin.conversations:write": {"admin.conversations.archive", "admin.conversations.bulkArchive", "admin.conversations.bulkDelete", "admin.conversations.bulkMove", "admin.conversations.convertToPrivate", "admin.conversations.convertToPublic", "admin.conversations.create", "admin.conversations.delete", "admin.conversations.disconnectShared", "admin.conversations.invite", "admin.conversations.removeCustomRetention", "admin.conversations.rename", "admin.conversations.restrictAccess.addGroup", "admin.conversations.restrictAccess.removeGroup", "admin.conversations.setConversationPrefs", "admin.conversations.setCustomRetention", "admin.conversations.setTeams", "admin.conversations.unarchive"},
"admin.conversations:read": {"admin.conversations.ekm.listOriginalConnectedChannelInfo", "admin.conversations.getConversationPrefs", "admin.conversations.getCustomRetention", "admin.conversations.getTeams", "admin.conversations.lookup", "admin.conversations.restrictAccess.listGroups", "admin.conversations.search"},
"admin.teams:write": {"admin.emoji.add", "admin.emoji.addAlias", "admin.emoji.remove", "admin.emoji.rename", "admin.teams.create", "admin.teams.settings.setDefaultChannels", "admin.teams.settings.setDescription", "admin.teams.settings.setDiscoverability", "admin.teams.settings.setIcon", "admin.teams.settings.setName", "admin.usergroups.addTeams"},
"admin.teams:read": {"admin.emoji.list", "admin.teams.admins.list", "admin.teams.list", "admin.teams.owners.list", "admin.teams.settings.info"},
"admin.workflows:read": {"admin.functions.list", "admin.functions.permissions.lookup", "admin.workflows.permissions.lookup", "admin.workflows.search"},
"admin.workflows:write": {"admin.functions.permissions.set", "admin.workflows.collaborators.add", "admin.workflows.collaborators.remove", "admin.workflows.unpublish"},
"admin.invites:write": {"admin.inviteRequests.approve", "admin.inviteRequests.deny"},
"admin.invites:read": {"admin.inviteRequests.approved.list", "admin.inviteRequests.denied.list", "admin.inviteRequests.list"},
"admin.roles:write": {"admin.roles.addAssignments", "admin.roles.removeAssignments"},
"admin.roles:read": {"admin.roles.listAssignments"},
"admin.usergroups:write": {"admin.usergroups.addChannels", "admin.usergroups.removeChannels"},
"admin.usergroups:read": {"admin.usergroups.listChannels"},
"hosting:read": {"apps.activities.list"},
"connections:write": {"apps.connections.open"},
"token": {"apps.datastore.bulkDelete", "apps.datastore.bulkGet", "apps.datastore.bulkPut", "apps.datastore.delete", "apps.datastore.get", "apps.datastore.put", "apps.datastore.query", "apps.datastore.update"},
"datastore:read": {"apps.datastore.count"},
"authorizations:read": {"apps.event.authorizations.list"},
"bot": {"auth.revoke", "auth.test", "chat.getPermalink", "chat.scheduledMessages.list", "dialog.open", "functions.completeError", "functions.completeSuccess", "rtm.connect", "rtm.start", "views.open", "views.publish", "views.push", "views.update"},
"bookmarks:write": {"bookmarks.add", "bookmarks.edit", "bookmarks.remove"},
"bookmarks:read": {"bookmarks.list"},
"users:read": {"bots.info", "users.getPresence", "users.info", "users.list"},
"calls:write": {"calls.add", "calls.end", "calls.participants.add", "calls.participants.remove", "calls.update"},
"calls:read": {"calls.info"},
"channels:manage": {"channels.create", "channels.mark", "conversations.archive", "conversations.close", "conversations.create", "conversations.kick", "conversations.leave", "conversations.mark", "conversations.open", "conversations.rename", "conversations.unarchive", "groups.create", "groups.mark", "im.mark", "im.open", "mpim.mark", "mpim.open"},
"channels:read": {"channels.info", "conversations.info", "conversations.list", "conversations.members", "groups.info", "im.list", "mpim.list", "users.conversations"},
"channels:write.invites": {"channels.invite", "conversations.invite", "groups.invite"},
"chat:write": {"chat.delete", "chat.deleteScheduledMessage", "chat.meMessage", "chat.postEphemeral", "chat.postMessage", "chat.scheduleMessage", "chat.update"},
"links:write": {"chat.unfurl"},
"conversations.connect:write": {"conversations.acceptSharedInvite", "conversations.inviteShared"},
"conversations.connect:manage": {"conversations.approveSharedInvite", "conversations.declineSharedInvite", "conversations.listConnectInvites"},
"channels:history": {"conversations.history", "conversations.replies"},
"channels:join": {"conversations.join"},
"channels:write.topic": {"conversations.setPurpose", "conversations.setTopic"},
"dnd:write": {"dnd.endDnd", "dnd.endSnooze", "dnd.setSnooze"},
"dnd:read": {"dnd.info", "dnd.teamInfo"},
"emoji:read": {"emoji.list"},
"files:write": {"files.comments.delete", "files.completeUploadExternal", "files.delete", "files.getUploadURLExternal", "files.revokePublicURL", "files.sharedPublicURL", "files.upload"},
"files:read": {"files.info", "files.list"},
"remote_files:write": {"files.remote.add", "files.remote.remove", "files.remote.update"},
"remote_files:read": {"files.remote.info", "files.remote.list"},
"remote_files:share": {"files.remote.share"},
"app_configurations:write": {"functions.distributions.permissions.add", "functions.distributions.permissions.remove", "functions.distributions.permissions.set"},
"app_configurations:read": {"functions.distributions.permissions.list"},
"conversations": {"groups.open"},
"tokens.basic": {"migration.exchange"},
"email": {"openid.connect.userInfo"},
"pins:write": {"pins.add", "pins.remove"},
"pins:read": {"pins.list"},
"reactions:write": {"reactions.add", "reactions.remove"},
"reactions:read": {"reactions.get", "reactions.list"},
"reminders:write": {"reminders.add", "reminders.complete", "reminders.delete"},
"reminders:read": {"reminders.info", "reminders.list"},
"search:read": {"search.all", "search.files", "search.messages"},
"stars:write": {"stars.add", "stars.remove"},
"stars:read": {"stars.list"},
"admin": {"team.accessLogs", "team.billableInfo", "team.integrationLogs"},
"team.billing:read": {"team.billing.info"},
"team:read": {"team.info"},
"team.preferences:read": {"team.preferences.list"},
"users.profile:read": {"team.profile.get", "users.profile.get"},
"usergroups:write": {"usergroups.create", "usergroups.disable", "usergroups.enable", "usergroups.update", "usergroups.users.update"},
"usergroups:read": {"usergroups.list", "usergroups.users.list"},
"users.profile:write": {"users.deletePhoto", "users.profile.set", "users.setPhoto"},
"identity.basic": {"users.identity"},
"users:read.email": {"users.lookupByEmail"},
"users:write": {"users.setActive", "users.setPresence"},
"workflow.steps:execute": {"workflows.stepCompleted", "workflows.stepFailed", "workflows.updateStep"},
"triggers:write": {"workflows.triggers.permissions.add", "workflows.triggers.permissions.remove", "workflows.triggers.permissions.set"},
"triggers:read": {"workflows.triggers.permissions.list"},
}
================================================
FILE: pkg/analyzer/analyzers/slack/slack.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go slack
package slack
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeSlack }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, errors.New("key not found in credentialInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeSlack,
Metadata: nil,
}
resourceType := "user"
fullyQualifiedName := info.User.TeamId + "/" + info.User.UserId
if info.User.BotId != "" {
resourceType = "bot"
fullyQualifiedName = info.User.BotId
}
resource := analyzers.Resource{
Name: info.User.User,
FullyQualifiedName: fullyQualifiedName,
Type: resourceType,
Metadata: map[string]any{
"url": info.User.Url,
"team": info.User.Team,
"team_id": info.User.TeamId,
"scopes": strings.Split(info.Scopes, ","),
},
}
// extract all permissions
permissions := extractPermissions(info)
result.Bindings = analyzers.BindAllPermissions(resource, permissions...)
return &result
}
func extractPermissions(info *SecretInfo) []analyzers.Permission {
var permissions []analyzers.Permission
for _, scope := range strings.Split(info.Scopes, ",") {
perms, ok := scope_mapping[scope]
if !ok {
continue
}
for _, perm := range perms {
if _, ok := StringToPermission[perm]; !ok {
// not in out generated permissions,
continue
}
permissions = append(permissions, analyzers.Permission{
Value: perm,
Parent: nil,
})
}
}
return permissions
}
// Add in showAll to printScopes + deal with testing enterprise + add scope details
type SlackUserData struct {
Ok bool `json:"ok"`
Url string `json:"url"`
Team string `json:"team"`
User string `json:"user"`
TeamId string `json:"team_id"`
UserId string `json:"user_id"`
BotId string `json:"bot_id"`
IsEnterprise bool `json:"is_enterprise"`
}
type SecretInfo struct {
Scopes string
User SlackUserData
}
func getSlackOAuthScopes(cfg *config.Config, key string) (scopes string, userData SlackUserData, err error) {
userData = SlackUserData{}
scopes = ""
// URL to which the request will be sent
url := "https://slack.com/api/auth.test"
// Create a client to send the request
client := analyzers.NewAnalyzeClient(cfg)
// Create the request
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return scopes, userData, err
}
// Add the Authorization header to the request
req.Header.Add("Authorization", "Bearer "+key)
// Send the request
resp, err := client.Do(req)
if err != nil {
return scopes, userData, err
}
defer resp.Body.Close() // Close the response body when the function returns
// print body
body, err := io.ReadAll(resp.Body)
if err != nil {
return scopes, userData, err
}
// Unmarshal the response body into the SlackUserData struct
if err := json.Unmarshal(body, &userData); err != nil {
return scopes, userData, err
}
// Print all headers received from the server
scopes = resp.Header.Get("X-Oauth-Scopes")
return scopes, userData, err
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error: %v", err)
return
}
color.Green("[!] Valid Slack API Key\n\n")
printIdentityInfo(info.User)
printScopes(strings.Split(info.Scopes, ","))
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
scopes, userData, err := getSlackOAuthScopes(cfg, key)
if err != nil {
return nil, fmt.Errorf("error getting Slack OAuth scopes: %w", err)
}
if !userData.Ok {
return nil, fmt.Errorf("invalid Slack token")
}
return &SecretInfo{
Scopes: scopes,
User: userData,
}, nil
}
func printIdentityInfo(userData SlackUserData) {
if userData.Url != "" {
color.Green("URL: %v", userData.Url)
}
if userData.Team != "" {
color.Green("Team: %v", userData.Team)
}
if userData.User != "" {
color.Green("User: %v", userData.User)
}
if userData.TeamId != "" {
color.Green("Team ID: %v", userData.TeamId)
}
if userData.UserId != "" {
color.Green("User ID: %v", userData.UserId)
}
if userData.BotId != "" {
color.Green("Bot ID: %v", userData.BotId)
}
fmt.Println("")
if userData.IsEnterprise {
color.Green("[!] Slack is Enterprise")
} else {
color.Yellow("[-] Slack is not Enterprise")
}
fmt.Println("")
}
func printScopes(scopes []string) {
t := table.NewWriter()
// if !showAll {
// t.SetOutputMirror(os.Stdout)
// t.AppendHeader(table.Row{"Scopes"})
// for _, scope := range scopes {
// t.AppendRow([]interface{}{color.GreenString(scope)})
// }
// } else {
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "Permissions"})
for _, scope := range scopes {
perms := scope_mapping[scope]
if perms == nil {
t.AppendRow([]interface{}{color.GreenString(scope), color.GreenString("")})
} else {
t.AppendRow([]interface{}{color.GreenString(scope), color.GreenString(strings.Join(perms, ", "))})
}
}
//}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/slack/slack_test.go
================================================
package slack
import (
_ "embed"
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid Slack key",
key: testSecrets.MustGetField("SLACK"),
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/sourcegraph/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package sourcegraph
import "errors"
type Permission int
const (
NoAccess Permission = iota
UserRead Permission = iota
SiteAdminFull Permission = iota
)
var (
PermissionStrings = map[Permission]string{
UserRead: "user:read",
SiteAdminFull: "site_admin:full",
}
StringToPermission = map[string]Permission{
"user:read": UserRead,
"site_admin:full": SiteAdminFull,
}
PermissionIDs = map[Permission]int{
UserRead: 0,
SiteAdminFull: 1,
}
IdToPermission = map[int]Permission{
0: UserRead,
1: SiteAdminFull,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/sourcegraph/permissions.yaml
================================================
permissions:
- user:read
- site_admin:full
================================================
FILE: pkg/analyzer/analyzers/sourcegraph/sourcegraph.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go sourcegraph
package sourcegraph
// ToDo: Add support for custom domain
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/fatih/color"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeSourcegraph }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, fmt.Errorf("missing key in credInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
permission := PermissionStrings[UserRead]
if info.IsSiteAdmin {
permission = PermissionStrings[SiteAdminFull]
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeSourcegraph,
Metadata: nil,
Bindings: []analyzers.Binding{
{
Resource: analyzers.Resource{
Name: info.User.Data.CurrentUser.Username,
FullyQualifiedName: "sourcegraph/" + info.User.Data.CurrentUser.Email,
Type: "user",
Metadata: map[string]any{
"created_at": info.User.Data.CurrentUser.CreatedAt,
"email": info.User.Data.CurrentUser.Email,
},
Parent: nil,
},
Permission: analyzers.Permission{
Value: permission,
},
},
},
}
return &result
}
type GraphQLError struct {
Message string `json:"message"`
Path []string `json:"path"`
}
type GraphQLResponse struct {
Errors []GraphQLError `json:"errors"`
Data interface{} `json:"data"`
}
type UserInfoJSON struct {
Data struct {
CurrentUser struct {
Username string `json:"username"`
Email string `json:"email"`
SiteAdmin bool `json:"siteAdmin"`
CreatedAt string `json:"createdAt"`
} `json:"currentUser"`
} `json:"data"`
}
type SecretInfo struct {
User UserInfoJSON
IsSiteAdmin bool
}
func getUserInfo(cfg *config.Config, key string) (UserInfoJSON, error) {
var userInfo UserInfoJSON
// POST request is considered as non-safe and sourcegraph has graphql APIs. They do not change any state.
// We are using unrestricted client to avoid error for non-safe API request.
client := analyzers.NewAnalyzeClientUnrestricted(cfg)
payload := "{\"query\":\"query { currentUser { username, email, siteAdmin, createdAt } }\"}"
req, err := http.NewRequest("POST", "https://sourcegraph.com/.api/graphql", strings.NewReader(payload))
if err != nil {
return userInfo, err
}
req.Header.Set("Authorization", "token "+key)
resp, err := client.Do(req)
if err != nil {
return userInfo, err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&userInfo)
if err != nil {
return userInfo, err
}
return userInfo, nil
}
func checkSiteAdmin(cfg *config.Config, key string) (bool, error) {
query := `
{
"query": "query webhooks($first: Int, $after: String, $kind: ExternalServiceKind) { webhooks(first: $first, after: $after, kind: $kind) { totalCount } }",
"variables": {
"first": 10,
"after": "",
"kind": "GITHUB"
}
}`
// POST request is considered as non-safe and sourcegraph has graphql APIs. They do not change any state.
// We are using unrestricted client to avoid error for non-safe API request.
client := analyzers.NewAnalyzeClientUnrestricted(cfg)
req, err := http.NewRequest("POST", "https://sourcegraph.com/.api/graphql", strings.NewReader(query))
if err != nil {
return false, err
}
req.Header.Set("Authorization", "token "+key)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
var response GraphQLResponse
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return false, err
}
if len(response.Errors) > 0 {
return false, nil
}
return true, nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
// ToDo: Add in logging
if cfg.LoggingEnabled {
color.Red("[x] Logging is not supported for this analyzer.")
return
}
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
color.Green("[!] Valid Sourcegraph Access Token\n\n")
color.Yellow("[i] Sourcegraph User Information\n")
color.Green("Username: %s\n", info.User.Data.CurrentUser.Username)
color.Green("Email: %s\n", info.User.Data.CurrentUser.Email)
color.Green("Created At: %s\n\n", info.User.Data.CurrentUser.CreatedAt)
if info.IsSiteAdmin {
color.Green("[!] Token Permissions: Site Admin")
} else {
// This is the default for all access tokens as of 6/11/24
color.Yellow("[i] Token Permissions: user:full (default)")
}
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
userInfo, err := getUserInfo(cfg, key)
if err != nil {
return nil, err
}
if userInfo.Data.CurrentUser.Username == "" {
return nil, fmt.Errorf("invalid Sourcegraph Access Token")
}
isSiteAdmin, err := checkSiteAdmin(cfg, key)
if err != nil {
return nil, err
}
return &SecretInfo{
User: userInfo,
IsSiteAdmin: isSiteAdmin,
}, nil
}
================================================
FILE: pkg/analyzer/analyzers/sourcegraph/sourcegraph_test.go
================================================
package sourcegraph
import (
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("SOURCEGRAPH")
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid SourceGraph key",
key: secret,
want: `{
"AnalyzerType": 17,
"Bindings": [
{
"Resource": {
"Name": "ahrav",
"FullyQualifiedName": "sourcegraph/ahravdutta02@gmail.com",
"Type": "user",
"Metadata": {
"created_at": "2023-07-23T04:16:31Z",
"email": "ahravdutta02@gmail.com"
},
"Parent": null
},
"Permission": {
"Value": "user:read",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": null
}`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
================================================
FILE: pkg/analyzer/analyzers/square/expected_output.json
================================================
{"AnalyzerType":18,"Bindings":[{"Resource":{"Name":"AcceptDispute","FullyQualifiedName":"AcceptDispute","Type":"endpoint","Metadata":null,"Parent":{"Name":"Disputes","FullyQualifiedName":"Disputes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"DISPUTES_WRITE","Parent":null}},{"Resource":{"Name":"AccumulateLoyaltyPoints","FullyQualifiedName":"AccumulateLoyaltyPoints","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_WRITE","Parent":null}},{"Resource":{"Name":"AddGroupToCustomer","FullyQualifiedName":"AddGroupToCustomer","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customers","FullyQualifiedName":"Customers","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"AdjustLoyaltyPoints","FullyQualifiedName":"AdjustLoyaltyPoints","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_WRITE","Parent":null}},{"Resource":{"Name":"BatchChangeInventory","FullyQualifiedName":"BatchChangeInventory","Type":"endpoint","Metadata":null,"Parent":{"Name":"Inventory","FullyQualifiedName":"Inventory","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVENTORY_WRITE","Parent":null}},{"Resource":{"Name":"BatchDeleteCatalogObjects","FullyQualifiedName":"BatchDeleteCatalogObjects","Type":"endpoint","Metadata":null,"Parent":{"Name":"Catalog","FullyQualifiedName":"Catalog","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_WRITE","Parent":null}},{"Resource":{"Name":"BatchRetrieveCatalogObjects","FullyQualifiedName":"BatchRetrieveCatalogObjects","Type":"endpoint","Metadata":null,"Parent":{"Name":"Catalog","FullyQualifiedName":"Catalog","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_READ","Parent":null}},{"Resource":{"Name":"BatchRetrieveInventoryChanges","FullyQualifiedName":"BatchRetrieveInventoryChanges","Type":"endpoint","Metadata":null,"Parent":{"Name":"Inventory","FullyQualifiedName":"Inventory","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVENTORY_READ","Parent":null}},{"Resource":{"Name":"BatchRetrieveInventoryCounts","FullyQualifiedName":"BatchRetrieveInventoryCounts","Type":"endpoint","Metadata":null,"Parent":{"Name":"Inventory","FullyQualifiedName":"Inventory","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVENTORY_READ","Parent":null}},{"Resource":{"Name":"BatchRetrieveOrders","FullyQualifiedName":"BatchRetrieveOrders","Type":"endpoint","Metadata":null,"Parent":{"Name":"Orders","FullyQualifiedName":"Orders","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_READ","Parent":null}},{"Resource":{"Name":"BatchUpsertCatalogObjects","FullyQualifiedName":"BatchUpsertCatalogObjects","Type":"endpoint","Metadata":null,"Parent":{"Name":"Catalog","FullyQualifiedName":"Catalog","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_WRITE","Parent":null}},{"Resource":{"Name":"BulkCreateCustomers","FullyQualifiedName":"BulkCreateCustomers","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customers","FullyQualifiedName":"Customers","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"BulkCreateTeamMembers","FullyQualifiedName":"BulkCreateTeamMembers","Type":"endpoint","Metadata":null,"Parent":{"Name":"Team","FullyQualifiedName":"Team","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"EMPLOYEES_WRITE","Parent":null}},{"Resource":{"Name":"BulkCreateVendors","FullyQualifiedName":"BulkCreateVendors","Type":"endpoint","Metadata":null,"Parent":{"Name":"Vendors","FullyQualifiedName":"Vendors","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"VENDOR_WRITE","Parent":null}},{"Resource":{"Name":"BulkDeleteCustomers","FullyQualifiedName":"BulkDeleteCustomers","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customers","FullyQualifiedName":"Customers","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"BulkDeleteLocationCustomAttributes","FullyQualifiedName":"BulkDeleteLocationCustomAttributes","Type":"endpoint","Metadata":null,"Parent":{"Name":"Location Custom Attributes","FullyQualifiedName":"Location Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"BulkDeleteMerchantCustomAttributes","FullyQualifiedName":"BulkDeleteMerchantCustomAttributes","Type":"endpoint","Metadata":null,"Parent":{"Name":"Merchant Custom Attributes","FullyQualifiedName":"Merchant Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"BulkDeleteOrderCustomAttributes","FullyQualifiedName":"BulkDeleteOrderCustomAttributes","Type":"endpoint","Metadata":null,"Parent":{"Name":"Order Custom Attributes","FullyQualifiedName":"Order Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"BulkRetrieveCustomers","FullyQualifiedName":"BulkRetrieveCustomers","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customers","FullyQualifiedName":"Customers","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"BulkRetrieveVendors","FullyQualifiedName":"BulkRetrieveVendors","Type":"endpoint","Metadata":null,"Parent":{"Name":"Vendors","FullyQualifiedName":"Vendors","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"VENDOR_READ","Parent":null}},{"Resource":{"Name":"BulkUpdateCustomers","FullyQualifiedName":"BulkUpdateCustomers","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customers","FullyQualifiedName":"Customers","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"BulkUpdateTeamMembers","FullyQualifiedName":"BulkUpdateTeamMembers","Type":"endpoint","Metadata":null,"Parent":{"Name":"Team","FullyQualifiedName":"Team","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"EMPLOYEES_WRITE","Parent":null}},{"Resource":{"Name":"BulkUpdateVendors","FullyQualifiedName":"BulkUpdateVendors","Type":"endpoint","Metadata":null,"Parent":{"Name":"Vendors","FullyQualifiedName":"Vendors","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"VENDOR_WRITE","Parent":null}},{"Resource":{"Name":"BulkUpsertBookingCustomAttributes (buyer-level)","FullyQualifiedName":"BulkUpsertBookingCustomAttributes (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"BulkUpsertBookingCustomAttributes (seller-level)","FullyQualifiedName":"BulkUpsertBookingCustomAttributes (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_WRITE","Parent":null}},{"Resource":{"Name":"BulkUpsertBookingCustomAttributes (seller-level)","FullyQualifiedName":"BulkUpsertBookingCustomAttributes (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"BulkUpsertCustomerCustomAttributes","FullyQualifiedName":"BulkUpsertCustomerCustomAttributes","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Custom Attributes","FullyQualifiedName":"Customer Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"BulkUpsertLocationCustomAttributes","FullyQualifiedName":"BulkUpsertLocationCustomAttributes","Type":"endpoint","Metadata":null,"Parent":{"Name":"Location Custom Attributes","FullyQualifiedName":"Location Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"BulkUpsertMerchantCustomAttributes","FullyQualifiedName":"BulkUpsertMerchantCustomAttributes","Type":"endpoint","Metadata":null,"Parent":{"Name":"Merchant Custom Attributes","FullyQualifiedName":"Merchant Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"BulkUpsertOrderCustomAttributes","FullyQualifiedName":"BulkUpsertOrderCustomAttributes","Type":"endpoint","Metadata":null,"Parent":{"Name":"Order Custom Attributes","FullyQualifiedName":"Order Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"CalculateLoyaltyPoints","FullyQualifiedName":"CalculateLoyaltyPoints","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_READ","Parent":null}},{"Resource":{"Name":"CancelBooking (buyer-level)","FullyQualifiedName":"CancelBooking (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CancelBooking (seller-level)","FullyQualifiedName":"CancelBooking (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_WRITE","Parent":null}},{"Resource":{"Name":"CancelBooking (seller-level)","FullyQualifiedName":"CancelBooking (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CancelInvoice","FullyQualifiedName":"CancelInvoice","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVOICES_WRITE","Parent":null}},{"Resource":{"Name":"CancelInvoice","FullyQualifiedName":"CancelInvoice","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"CancelLoyaltyPromotion","FullyQualifiedName":"CancelLoyaltyPromotion","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_WRITE","Parent":null}},{"Resource":{"Name":"CancelPayment","FullyQualifiedName":"CancelPayment","Type":"endpoint","Metadata":null,"Parent":{"Name":"Payments and Refunds","FullyQualifiedName":"Payments and Refunds","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CancelPaymentByIdempotencyKey","FullyQualifiedName":"CancelPaymentByIdempotencyKey","Type":"endpoint","Metadata":null,"Parent":{"Name":"Payments and Refunds","FullyQualifiedName":"Payments and Refunds","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CancelSubscription","FullyQualifiedName":"CancelSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"SUBSCRIPTIONS_WRITE","Parent":null}},{"Resource":{"Name":"CancelTerminalAction","FullyQualifiedName":"CancelTerminalAction","Type":"endpoint","Metadata":null,"Parent":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CancelTerminalCheckout","FullyQualifiedName":"CancelTerminalCheckout","Type":"endpoint","Metadata":null,"Parent":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CancelTerminalRefund","FullyQualifiedName":"CancelTerminalRefund","Type":"endpoint","Metadata":null,"Parent":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CatalogInfo","FullyQualifiedName":"CatalogInfo","Type":"endpoint","Metadata":null,"Parent":{"Name":"Catalog","FullyQualifiedName":"Catalog","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_READ","Parent":null}},{"Resource":{"Name":"CloneOrder","FullyQualifiedName":"CloneOrder","Type":"endpoint","Metadata":null,"Parent":{"Name":"Orders","FullyQualifiedName":"Orders","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"CompletePayment","FullyQualifiedName":"CompletePayment","Type":"endpoint","Metadata":null,"Parent":{"Name":"Payments and Refunds","FullyQualifiedName":"Payments and Refunds","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CreateBooking (buyer-level)","FullyQualifiedName":"CreateBooking (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CreateBooking (seller-level)","FullyQualifiedName":"CreateBooking (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_WRITE","Parent":null}},{"Resource":{"Name":"CreateBooking (seller-level)","FullyQualifiedName":"CreateBooking (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CreateBookingCustomAttributeDefinition (buyer-level)","FullyQualifiedName":"CreateBookingCustomAttributeDefinition (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CreateBookingCustomAttributeDefinition (seller-level)","FullyQualifiedName":"CreateBookingCustomAttributeDefinition (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_WRITE","Parent":null}},{"Resource":{"Name":"CreateBookingCustomAttributeDefinition (seller-level)","FullyQualifiedName":"CreateBookingCustomAttributeDefinition (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CreateBreakType","FullyQualifiedName":"CreateBreakType","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"TIMECARDS_SETTINGS_WRITE","Parent":null}},{"Resource":{"Name":"CreateCard","FullyQualifiedName":"CreateCard","Type":"endpoint","Metadata":null,"Parent":{"Name":"Cards","FullyQualifiedName":"Cards","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CreateCatalogImage","FullyQualifiedName":"CreateCatalogImage","Type":"endpoint","Metadata":null,"Parent":{"Name":"Catalog","FullyQualifiedName":"Catalog","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_WRITE","Parent":null}},{"Resource":{"Name":"CreateCustomer","FullyQualifiedName":"CreateCustomer","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customers","FullyQualifiedName":"Customers","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"CreateCustomerCard (deprecated)","FullyQualifiedName":"CreateCustomerCard (deprecated)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customers","FullyQualifiedName":"Customers","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"CreateCustomerCustomAttributeDefinition","FullyQualifiedName":"CreateCustomerCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Custom Attributes","FullyQualifiedName":"Customer Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"CreateCustomerGroup","FullyQualifiedName":"CreateCustomerGroup","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Groups","FullyQualifiedName":"Customer Groups","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"CreateDeviceCode","FullyQualifiedName":"CreateDeviceCode","Type":"endpoint","Metadata":null,"Parent":{"Name":"Devices","FullyQualifiedName":"Devices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"DEVICE_CREDENTIAL_MANAGEMENT","Parent":null}},{"Resource":{"Name":"CreateDisputeEvidenceFile","FullyQualifiedName":"CreateDisputeEvidenceFile","Type":"endpoint","Metadata":null,"Parent":{"Name":"Disputes","FullyQualifiedName":"Disputes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"DISPUTES_WRITE","Parent":null}},{"Resource":{"Name":"CreateDisputeEvidenceText","FullyQualifiedName":"CreateDisputeEvidenceText","Type":"endpoint","Metadata":null,"Parent":{"Name":"Disputes","FullyQualifiedName":"Disputes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"DISPUTES_WRITE","Parent":null}},{"Resource":{"Name":"CreateGiftCard","FullyQualifiedName":"CreateGiftCard","Type":"endpoint","Metadata":null,"Parent":{"Name":"Gift Cards","FullyQualifiedName":"Gift Cards","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"GIFTCARDS_WRITE","Parent":null}},{"Resource":{"Name":"CreateGiftCardActivity","FullyQualifiedName":"CreateGiftCardActivity","Type":"endpoint","Metadata":null,"Parent":{"Name":"Gift Card Activities","FullyQualifiedName":"Gift Card Activities","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"GIFTCARDS_WRITE","Parent":null}},{"Resource":{"Name":"CreateInvoice","FullyQualifiedName":"CreateInvoice","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVOICES_WRITE","Parent":null}},{"Resource":{"Name":"CreateInvoice","FullyQualifiedName":"CreateInvoice","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"CreateInvoiceAttachment","FullyQualifiedName":"CreateInvoiceAttachment","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVOICES_WRITE","Parent":null}},{"Resource":{"Name":"CreateInvoiceAttachment","FullyQualifiedName":"CreateInvoiceAttachment","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"CreateLocation","FullyQualifiedName":"CreateLocation","Type":"endpoint","Metadata":null,"Parent":{"Name":"Locations","FullyQualifiedName":"Locations","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"CreateLocationCustomAttributeDefinition","FullyQualifiedName":"CreateLocationCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Location Custom Attributes","FullyQualifiedName":"Location Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"CreateLoyaltyAccount","FullyQualifiedName":"CreateLoyaltyAccount","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_WRITE","Parent":null}},{"Resource":{"Name":"CreateLoyaltyPromotion","FullyQualifiedName":"CreateLoyaltyPromotion","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_WRITE","Parent":null}},{"Resource":{"Name":"CreateLoyaltyReward","FullyQualifiedName":"CreateLoyaltyReward","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_WRITE","Parent":null}},{"Resource":{"Name":"CreateMerchantCustomAttributeDefinition","FullyQualifiedName":"CreateMerchantCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Merchant Custom Attributes","FullyQualifiedName":"Merchant Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"CreateMobileAuthorizationCode","FullyQualifiedName":"CreateMobileAuthorizationCode","Type":"endpoint","Metadata":null,"Parent":{"Name":"Mobile Authorization","FullyQualifiedName":"Mobile Authorization","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE_IN_PERSON","Parent":null}},{"Resource":{"Name":"CreateOrder","FullyQualifiedName":"CreateOrder","Type":"endpoint","Metadata":null,"Parent":{"Name":"Orders","FullyQualifiedName":"Orders","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"CreateOrderCustomAttributeDefinition","FullyQualifiedName":"CreateOrderCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Order Custom Attributes","FullyQualifiedName":"Order Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"CreatePayment","FullyQualifiedName":"CreatePayment","Type":"endpoint","Metadata":null,"Parent":{"Name":"Payments and Refunds","FullyQualifiedName":"Payments and Refunds","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CreatePayment","FullyQualifiedName":"CreatePayment","Type":"endpoint","Metadata":null,"Parent":{"Name":"Payments and Refunds","FullyQualifiedName":"Payments and Refunds","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE_ADDITIONAL_RECIPIENTS","Parent":null}},{"Resource":{"Name":"CreatePayment","FullyQualifiedName":"CreatePayment","Type":"endpoint","Metadata":null,"Parent":{"Name":"Payments and Refunds","FullyQualifiedName":"Payments and Refunds","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE_SHARED_ONFILE","Parent":null}},{"Resource":{"Name":"CreatePaymentLink","FullyQualifiedName":"CreatePaymentLink","Type":"endpoint","Metadata":null,"Parent":{"Name":"Checkout","FullyQualifiedName":"Checkout","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_READ","Parent":null}},{"Resource":{"Name":"CreatePaymentLink","FullyQualifiedName":"CreatePaymentLink","Type":"endpoint","Metadata":null,"Parent":{"Name":"Checkout","FullyQualifiedName":"Checkout","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"CreatePaymentLink","FullyQualifiedName":"CreatePaymentLink","Type":"endpoint","Metadata":null,"Parent":{"Name":"Checkout","FullyQualifiedName":"Checkout","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CreateShift","FullyQualifiedName":"CreateShift","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"TIMECARDS_WRITE","Parent":null}},{"Resource":{"Name":"CreateSubscription","FullyQualifiedName":"CreateSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"CreateSubscription","FullyQualifiedName":"CreateSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVOICES_WRITE","Parent":null}},{"Resource":{"Name":"CreateSubscription","FullyQualifiedName":"CreateSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_READ","Parent":null}},{"Resource":{"Name":"CreateSubscription","FullyQualifiedName":"CreateSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"CreateSubscription","FullyQualifiedName":"CreateSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CreateSubscription","FullyQualifiedName":"CreateSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"SUBSCRIPTIONS_WRITE","Parent":null}},{"Resource":{"Name":"CreateTeamMember","FullyQualifiedName":"CreateTeamMember","Type":"endpoint","Metadata":null,"Parent":{"Name":"Team","FullyQualifiedName":"Team","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"EMPLOYEES_WRITE","Parent":null}},{"Resource":{"Name":"CreateTerminalAction","FullyQualifiedName":"CreateTerminalAction","Type":"endpoint","Metadata":null,"Parent":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CreateTerminalCheckout","FullyQualifiedName":"CreateTerminalCheckout","Type":"endpoint","Metadata":null,"Parent":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CreateTerminalRefund","FullyQualifiedName":"CreateTerminalRefund","Type":"endpoint","Metadata":null,"Parent":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"CreateVendor","FullyQualifiedName":"CreateVendor","Type":"endpoint","Metadata":null,"Parent":{"Name":"Vendors","FullyQualifiedName":"Vendors","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"VENDOR_WRITE","Parent":null}},{"Resource":{"Name":"DeleteBookingCustomAttribute (buyer-level)","FullyQualifiedName":"DeleteBookingCustomAttribute (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteBookingCustomAttribute (seller-level)","FullyQualifiedName":"DeleteBookingCustomAttribute (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_WRITE","Parent":null}},{"Resource":{"Name":"DeleteBookingCustomAttribute (seller-level)","FullyQualifiedName":"DeleteBookingCustomAttribute (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteBookingCustomAttributeDefinition (buyer-level)","FullyQualifiedName":"DeleteBookingCustomAttributeDefinition (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteBookingCustomAttributeDefinition (seller-level)","FullyQualifiedName":"DeleteBookingCustomAttributeDefinition (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_WRITE","Parent":null}},{"Resource":{"Name":"DeleteBookingCustomAttributeDefinition (seller-level)","FullyQualifiedName":"DeleteBookingCustomAttributeDefinition (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteBreakType","FullyQualifiedName":"DeleteBreakType","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"TIMECARDS_SETTINGS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteCatalogObject","FullyQualifiedName":"DeleteCatalogObject","Type":"endpoint","Metadata":null,"Parent":{"Name":"Catalog","FullyQualifiedName":"Catalog","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteCustomer","FullyQualifiedName":"DeleteCustomer","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customers","FullyQualifiedName":"Customers","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteCustomerCard (deprecated)","FullyQualifiedName":"DeleteCustomerCard (deprecated)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customers","FullyQualifiedName":"Customers","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteCustomerCustomAttribute","FullyQualifiedName":"DeleteCustomerCustomAttribute","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Custom Attributes","FullyQualifiedName":"Customer Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteCustomerCustomAttributeDefinition","FullyQualifiedName":"DeleteCustomerCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Custom Attributes","FullyQualifiedName":"Customer Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteCustomerGroup","FullyQualifiedName":"DeleteCustomerGroup","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Groups","FullyQualifiedName":"Customer Groups","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteDisputeEvidence","FullyQualifiedName":"DeleteDisputeEvidence","Type":"endpoint","Metadata":null,"Parent":{"Name":"Disputes","FullyQualifiedName":"Disputes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"DISPUTES_WRITE","Parent":null}},{"Resource":{"Name":"DeleteInvoice","FullyQualifiedName":"DeleteInvoice","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVOICES_WRITE","Parent":null}},{"Resource":{"Name":"DeleteInvoice","FullyQualifiedName":"DeleteInvoice","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteInvoiceAttachment","FullyQualifiedName":"DeleteInvoiceAttachment","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVOICES_WRITE","Parent":null}},{"Resource":{"Name":"DeleteInvoiceAttachment","FullyQualifiedName":"DeleteInvoiceAttachment","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteLocationCustomAttribute","FullyQualifiedName":"DeleteLocationCustomAttribute","Type":"endpoint","Metadata":null,"Parent":{"Name":"Location Custom Attributes","FullyQualifiedName":"Location Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"DeleteLocationCustomAttributeDefinition","FullyQualifiedName":"DeleteLocationCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Location Custom Attributes","FullyQualifiedName":"Location Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"DeleteLoyaltyReward","FullyQualifiedName":"DeleteLoyaltyReward","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_WRITE","Parent":null}},{"Resource":{"Name":"DeleteMerchantCustomAttribute","FullyQualifiedName":"DeleteMerchantCustomAttribute","Type":"endpoint","Metadata":null,"Parent":{"Name":"Merchant Custom Attributes","FullyQualifiedName":"Merchant Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"DeleteMerchantCustomAttributeDefinition","FullyQualifiedName":"DeleteMerchantCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Merchant Custom Attributes","FullyQualifiedName":"Merchant Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"DeleteOrderCustomAttribute","FullyQualifiedName":"DeleteOrderCustomAttribute","Type":"endpoint","Metadata":null,"Parent":{"Name":"Order Custom Attributes","FullyQualifiedName":"Order Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteOrderCustomAttributeDefinition","FullyQualifiedName":"DeleteOrderCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Order Custom Attributes","FullyQualifiedName":"Order Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteShift","FullyQualifiedName":"DeleteShift","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"TIMECARDS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteSnippet","FullyQualifiedName":"DeleteSnippet","Type":"endpoint","Metadata":null,"Parent":{"Name":"Snippets","FullyQualifiedName":"Snippets","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ONLINE_STORE_SNIPPETS_WRITE","Parent":null}},{"Resource":{"Name":"DeleteSubscriptionAction","FullyQualifiedName":"DeleteSubscriptionAction","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"SUBSCRIPTIONS_WRITE","Parent":null}},{"Resource":{"Name":"DisableCard","FullyQualifiedName":"DisableCard","Type":"endpoint","Metadata":null,"Parent":{"Name":"Cards","FullyQualifiedName":"Cards","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"GetBankAccount","FullyQualifiedName":"GetBankAccount","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bank Accounts","FullyQualifiedName":"Bank Accounts","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"BANK_ACCOUNTS_READ","Parent":null}},{"Resource":{"Name":"GetBankAccountByV1Id","FullyQualifiedName":"GetBankAccountByV1Id","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bank Accounts","FullyQualifiedName":"Bank Accounts","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"BANK_ACCOUNTS_READ","Parent":null}},{"Resource":{"Name":"GetBreakType","FullyQualifiedName":"GetBreakType","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"TIMECARDS_SETTINGS_READ","Parent":null}},{"Resource":{"Name":"GetDevice","FullyQualifiedName":"GetDevice","Type":"endpoint","Metadata":null,"Parent":{"Name":"Devices","FullyQualifiedName":"Devices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"DEVICES_READ","Parent":null}},{"Resource":{"Name":"GetDeviceCode","FullyQualifiedName":"GetDeviceCode","Type":"endpoint","Metadata":null,"Parent":{"Name":"Devices","FullyQualifiedName":"Devices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"DEVICE_CREDENTIAL_MANAGEMENT","Parent":null}},{"Resource":{"Name":"GetInvoice","FullyQualifiedName":"GetInvoice","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVOICES_READ","Parent":null}},{"Resource":{"Name":"GetPayment","FullyQualifiedName":"GetPayment","Type":"endpoint","Metadata":null,"Parent":{"Name":"Payments and Refunds","FullyQualifiedName":"Payments and Refunds","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_READ","Parent":null}},{"Resource":{"Name":"GetPaymentRefund","FullyQualifiedName":"GetPaymentRefund","Type":"endpoint","Metadata":null,"Parent":{"Name":"Payments and Refunds","FullyQualifiedName":"Payments and Refunds","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_READ","Parent":null}},{"Resource":{"Name":"GetPayout","FullyQualifiedName":"GetPayout","Type":"endpoint","Metadata":null,"Parent":{"Name":"Payouts","FullyQualifiedName":"Payouts","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYOUTS_READ","Parent":null}},{"Resource":{"Name":"GetShift","FullyQualifiedName":"GetShift","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"TIMECARDS_READ","Parent":null}},{"Resource":{"Name":"GetTeamMemberWage","FullyQualifiedName":"GetTeamMemberWage","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"EMPLOYEES_READ","Parent":null}},{"Resource":{"Name":"GetTerminalAction","FullyQualifiedName":"GetTerminalAction","Type":"endpoint","Metadata":null,"Parent":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"GetTerminalAction","FullyQualifiedName":"GetTerminalAction","Type":"endpoint","Metadata":null,"Parent":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_READ","Parent":null}},{"Resource":{"Name":"GetTerminalCheckout","FullyQualifiedName":"GetTerminalCheckout","Type":"endpoint","Metadata":null,"Parent":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_READ","Parent":null}},{"Resource":{"Name":"GetTerminalRefund","FullyQualifiedName":"GetTerminalRefund","Type":"endpoint","Metadata":null,"Parent":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_READ","Parent":null}},{"Resource":{"Name":"LinkCustomerToGiftCard","FullyQualifiedName":"LinkCustomerToGiftCard","Type":"endpoint","Metadata":null,"Parent":{"Name":"Gift Cards","FullyQualifiedName":"Gift Cards","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"GIFTCARDS_WRITE","Parent":null}},{"Resource":{"Name":"ListBankAccounts","FullyQualifiedName":"ListBankAccounts","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bank Accounts","FullyQualifiedName":"Bank Accounts","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"BANK_ACCOUNTS_READ","Parent":null}},{"Resource":{"Name":"ListBookingCustomAttributeDefinitions (buyer-level)","FullyQualifiedName":"ListBookingCustomAttributeDefinitions (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_READ","Parent":null}},{"Resource":{"Name":"ListBookingCustomAttributeDefinitions (seller-level)","FullyQualifiedName":"ListBookingCustomAttributeDefinitions (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_READ","Parent":null}},{"Resource":{"Name":"ListBookingCustomAttributeDefinitions (seller-level)","FullyQualifiedName":"ListBookingCustomAttributeDefinitions (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_READ","Parent":null}},{"Resource":{"Name":"ListBookingCustomAttributes (buyer-level)","FullyQualifiedName":"ListBookingCustomAttributes (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_READ","Parent":null}},{"Resource":{"Name":"ListBookingCustomAttributes (seller-level)","FullyQualifiedName":"ListBookingCustomAttributes (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_READ","Parent":null}},{"Resource":{"Name":"ListBookingCustomAttributes (seller-level)","FullyQualifiedName":"ListBookingCustomAttributes (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_READ","Parent":null}},{"Resource":{"Name":"ListBookings (buyer-level)","FullyQualifiedName":"ListBookings (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_READ","Parent":null}},{"Resource":{"Name":"ListBookings (seller-level)","FullyQualifiedName":"ListBookings (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_READ","Parent":null}},{"Resource":{"Name":"ListBookings (seller-level)","FullyQualifiedName":"ListBookings (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_READ","Parent":null}},{"Resource":{"Name":"ListBreakTypes","FullyQualifiedName":"ListBreakTypes","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"TIMECARDS_SETTINGS_READ","Parent":null}},{"Resource":{"Name":"ListCards","FullyQualifiedName":"ListCards","Type":"endpoint","Metadata":null,"Parent":{"Name":"Cards","FullyQualifiedName":"Cards","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_READ","Parent":null}},{"Resource":{"Name":"ListCashDrawerShiftEvents","FullyQualifiedName":"ListCashDrawerShiftEvents","Type":"endpoint","Metadata":null,"Parent":{"Name":"Cash Drawer Shifts","FullyQualifiedName":"Cash Drawer Shifts","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CASH_DRAWER_READ","Parent":null}},{"Resource":{"Name":"ListCashDrawerShifts","FullyQualifiedName":"ListCashDrawerShifts","Type":"endpoint","Metadata":null,"Parent":{"Name":"Cash Drawer Shifts","FullyQualifiedName":"Cash Drawer Shifts","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CASH_DRAWER_READ","Parent":null}},{"Resource":{"Name":"ListCatalog","FullyQualifiedName":"ListCatalog","Type":"endpoint","Metadata":null,"Parent":{"Name":"Catalog","FullyQualifiedName":"Catalog","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_READ","Parent":null}},{"Resource":{"Name":"ListCustomerCustomAttributeDefinitions","FullyQualifiedName":"ListCustomerCustomAttributeDefinitions","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Custom Attributes","FullyQualifiedName":"Customer Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"ListCustomerCustomAttributes","FullyQualifiedName":"ListCustomerCustomAttributes","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Custom Attributes","FullyQualifiedName":"Customer Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"ListCustomerGroups","FullyQualifiedName":"ListCustomerGroups","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Groups","FullyQualifiedName":"Customer Groups","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"ListCustomerSegments","FullyQualifiedName":"ListCustomerSegments","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Segments","FullyQualifiedName":"Customer Segments","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"ListCustomers","FullyQualifiedName":"ListCustomers","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customers","FullyQualifiedName":"Customers","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"ListDeviceCodes","FullyQualifiedName":"ListDeviceCodes","Type":"endpoint","Metadata":null,"Parent":{"Name":"Devices","FullyQualifiedName":"Devices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"DEVICE_CREDENTIAL_MANAGEMENT","Parent":null}},{"Resource":{"Name":"ListDevices","FullyQualifiedName":"ListDevices","Type":"endpoint","Metadata":null,"Parent":{"Name":"Devices","FullyQualifiedName":"Devices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"DEVICES_READ","Parent":null}},{"Resource":{"Name":"ListDisputeEvidence","FullyQualifiedName":"ListDisputeEvidence","Type":"endpoint","Metadata":null,"Parent":{"Name":"Disputes","FullyQualifiedName":"Disputes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"DISPUTES_READ","Parent":null}},{"Resource":{"Name":"ListDisputes","FullyQualifiedName":"ListDisputes","Type":"endpoint","Metadata":null,"Parent":{"Name":"Disputes","FullyQualifiedName":"Disputes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"DISPUTES_READ","Parent":null}},{"Resource":{"Name":"ListEmployees (deprecated)","FullyQualifiedName":"ListEmployees (deprecated)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Employees","FullyQualifiedName":"Employees","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"EMPLOYEES_READ","Parent":null}},{"Resource":{"Name":"ListGiftCardActivities","FullyQualifiedName":"ListGiftCardActivities","Type":"endpoint","Metadata":null,"Parent":{"Name":"Gift Card Activities","FullyQualifiedName":"Gift Card Activities","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"GIFTCARDS_READ","Parent":null}},{"Resource":{"Name":"ListGiftCards","FullyQualifiedName":"ListGiftCards","Type":"endpoint","Metadata":null,"Parent":{"Name":"Gift Cards","FullyQualifiedName":"Gift Cards","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"GIFTCARDS_READ","Parent":null}},{"Resource":{"Name":"ListInvoices","FullyQualifiedName":"ListInvoices","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVOICES_READ","Parent":null}},{"Resource":{"Name":"ListLocationCustomAttributeDefinitions","FullyQualifiedName":"ListLocationCustomAttributeDefinitions","Type":"endpoint","Metadata":null,"Parent":{"Name":"Location Custom Attributes","FullyQualifiedName":"Location Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_READ","Parent":null}},{"Resource":{"Name":"ListLocationCustomAttributes","FullyQualifiedName":"ListLocationCustomAttributes","Type":"endpoint","Metadata":null,"Parent":{"Name":"Location Custom Attributes","FullyQualifiedName":"Location Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_READ","Parent":null}},{"Resource":{"Name":"ListLocations","FullyQualifiedName":"ListLocations","Type":"endpoint","Metadata":null,"Parent":{"Name":"Locations","FullyQualifiedName":"Locations","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_READ","Parent":null}},{"Resource":{"Name":"ListLoyaltyPrograms (deprecated)","FullyQualifiedName":"ListLoyaltyPrograms (deprecated)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_READ","Parent":null}},{"Resource":{"Name":"ListLoyaltyPromotions","FullyQualifiedName":"ListLoyaltyPromotions","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_READ","Parent":null}},{"Resource":{"Name":"ListMerchantCustomAttributeDefinitions","FullyQualifiedName":"ListMerchantCustomAttributeDefinitions","Type":"endpoint","Metadata":null,"Parent":{"Name":"Merchant Custom Attributes","FullyQualifiedName":"Merchant Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_READ","Parent":null}},{"Resource":{"Name":"ListMerchantCustomAttributes","FullyQualifiedName":"ListMerchantCustomAttributes","Type":"endpoint","Metadata":null,"Parent":{"Name":"Merchant Custom Attributes","FullyQualifiedName":"Merchant Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_READ","Parent":null}},{"Resource":{"Name":"ListMerchants","FullyQualifiedName":"ListMerchants","Type":"endpoint","Metadata":null,"Parent":{"Name":"Merchants","FullyQualifiedName":"Merchants","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_READ","Parent":null}},{"Resource":{"Name":"ListOrderCustomAttributeDefinitions","FullyQualifiedName":"ListOrderCustomAttributeDefinitions","Type":"endpoint","Metadata":null,"Parent":{"Name":"Order Custom Attributes","FullyQualifiedName":"Order Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_READ","Parent":null}},{"Resource":{"Name":"ListOrderCustomAttributes","FullyQualifiedName":"ListOrderCustomAttributes","Type":"endpoint","Metadata":null,"Parent":{"Name":"Order Custom Attributes","FullyQualifiedName":"Order Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_READ","Parent":null}},{"Resource":{"Name":"ListPaymentRefunds","FullyQualifiedName":"ListPaymentRefunds","Type":"endpoint","Metadata":null,"Parent":{"Name":"Payments and Refunds","FullyQualifiedName":"Payments and Refunds","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_READ","Parent":null}},{"Resource":{"Name":"ListPayments","FullyQualifiedName":"ListPayments","Type":"endpoint","Metadata":null,"Parent":{"Name":"Payments and Refunds","FullyQualifiedName":"Payments and Refunds","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_READ","Parent":null}},{"Resource":{"Name":"ListPayoutEntries","FullyQualifiedName":"ListPayoutEntries","Type":"endpoint","Metadata":null,"Parent":{"Name":"Payouts","FullyQualifiedName":"Payouts","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYOUTS_READ","Parent":null}},{"Resource":{"Name":"ListPayouts","FullyQualifiedName":"ListPayouts","Type":"endpoint","Metadata":null,"Parent":{"Name":"Payouts","FullyQualifiedName":"Payouts","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYOUTS_READ","Parent":null}},{"Resource":{"Name":"ListSites","FullyQualifiedName":"ListSites","Type":"endpoint","Metadata":null,"Parent":{"Name":"Sites","FullyQualifiedName":"Sites","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ONLINE_STORE_SITE_READ","Parent":null}},{"Resource":{"Name":"ListSubscriptionEvents","FullyQualifiedName":"ListSubscriptionEvents","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"SUBSCRIPTIONS_READ","Parent":null}},{"Resource":{"Name":"ListTeamMemberBookingProfiles","FullyQualifiedName":"ListTeamMemberBookingProfiles","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_BUSINESS_SETTINGS_READ","Parent":null}},{"Resource":{"Name":"ListTeamMemberWages","FullyQualifiedName":"ListTeamMemberWages","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"EMPLOYEES_READ","Parent":null}},{"Resource":{"Name":"ListWorkweekConfigs","FullyQualifiedName":"ListWorkweekConfigs","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"TIMECARDS_SETTINGS_READ","Parent":null}},{"Resource":{"Name":"PauseSubscription","FullyQualifiedName":"PauseSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"PauseSubscription","FullyQualifiedName":"PauseSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVOICES_WRITE","Parent":null}},{"Resource":{"Name":"PauseSubscription","FullyQualifiedName":"PauseSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_READ","Parent":null}},{"Resource":{"Name":"PauseSubscription","FullyQualifiedName":"PauseSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"PauseSubscription","FullyQualifiedName":"PauseSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"PauseSubscription","FullyQualifiedName":"PauseSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"SUBSCRIPTIONS_WRITE","Parent":null}},{"Resource":{"Name":"PayOrder","FullyQualifiedName":"PayOrder","Type":"endpoint","Metadata":null,"Parent":{"Name":"Orders","FullyQualifiedName":"Orders","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"PayOrder","FullyQualifiedName":"PayOrder","Type":"endpoint","Metadata":null,"Parent":{"Name":"Orders","FullyQualifiedName":"Orders","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"PublishInvoice","FullyQualifiedName":"PublishInvoice","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"PublishInvoice","FullyQualifiedName":"PublishInvoice","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVOICES_WRITE","Parent":null}},{"Resource":{"Name":"PublishInvoice","FullyQualifiedName":"PublishInvoice","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"PublishInvoice","FullyQualifiedName":"PublishInvoice","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"RedeemLoyaltyReward","FullyQualifiedName":"RedeemLoyaltyReward","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_WRITE","Parent":null}},{"Resource":{"Name":"RefundPayment","FullyQualifiedName":"RefundPayment","Type":"endpoint","Metadata":null,"Parent":{"Name":"Payments and Refunds","FullyQualifiedName":"Payments and Refunds","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"RefundPayment","FullyQualifiedName":"RefundPayment","Type":"endpoint","Metadata":null,"Parent":{"Name":"Payments and Refunds","FullyQualifiedName":"Payments and Refunds","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE_ADDITIONAL_RECIPIENTS","Parent":null}},{"Resource":{"Name":"RemoveGroupFromCustomer","FullyQualifiedName":"RemoveGroupFromCustomer","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customers","FullyQualifiedName":"Customers","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"ResumeSubscription","FullyQualifiedName":"ResumeSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"ResumeSubscription","FullyQualifiedName":"ResumeSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVOICES_WRITE","Parent":null}},{"Resource":{"Name":"ResumeSubscription","FullyQualifiedName":"ResumeSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_READ","Parent":null}},{"Resource":{"Name":"ResumeSubscription","FullyQualifiedName":"ResumeSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"ResumeSubscription","FullyQualifiedName":"ResumeSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"ResumeSubscription","FullyQualifiedName":"ResumeSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"SUBSCRIPTIONS_WRITE","Parent":null}},{"Resource":{"Name":"RetrieveBooking (buyer-level)","FullyQualifiedName":"RetrieveBooking (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_READ","Parent":null}},{"Resource":{"Name":"RetrieveBooking (seller-level)","FullyQualifiedName":"RetrieveBooking (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_READ","Parent":null}},{"Resource":{"Name":"RetrieveBooking (seller-level)","FullyQualifiedName":"RetrieveBooking (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_READ","Parent":null}},{"Resource":{"Name":"RetrieveBookingCustomAttribute (buyer-level)","FullyQualifiedName":"RetrieveBookingCustomAttribute (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_READ","Parent":null}},{"Resource":{"Name":"RetrieveBookingCustomAttribute (seller-level)","FullyQualifiedName":"RetrieveBookingCustomAttribute (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_READ","Parent":null}},{"Resource":{"Name":"RetrieveBookingCustomAttribute (seller-level)","FullyQualifiedName":"RetrieveBookingCustomAttribute (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_READ","Parent":null}},{"Resource":{"Name":"RetrieveBookingCustomAttributeDefinition (buyer-level)","FullyQualifiedName":"RetrieveBookingCustomAttributeDefinition (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_READ","Parent":null}},{"Resource":{"Name":"RetrieveBookingCustomAttributeDefinition (seller-level)","FullyQualifiedName":"RetrieveBookingCustomAttributeDefinition (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_READ","Parent":null}},{"Resource":{"Name":"RetrieveBookingCustomAttributeDefinition (seller-level)","FullyQualifiedName":"RetrieveBookingCustomAttributeDefinition (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_READ","Parent":null}},{"Resource":{"Name":"RetrieveBusinessBookingProfile","FullyQualifiedName":"RetrieveBusinessBookingProfile","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_BUSINESS_SETTINGS_READ","Parent":null}},{"Resource":{"Name":"RetrieveCard","FullyQualifiedName":"RetrieveCard","Type":"endpoint","Metadata":null,"Parent":{"Name":"Cards","FullyQualifiedName":"Cards","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_READ","Parent":null}},{"Resource":{"Name":"RetrieveCashDrawerShift","FullyQualifiedName":"RetrieveCashDrawerShift","Type":"endpoint","Metadata":null,"Parent":{"Name":"Cash Drawer Shifts","FullyQualifiedName":"Cash Drawer Shifts","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CASH_DRAWER_READ","Parent":null}},{"Resource":{"Name":"RetrieveCatalogObject","FullyQualifiedName":"RetrieveCatalogObject","Type":"endpoint","Metadata":null,"Parent":{"Name":"Catalog","FullyQualifiedName":"Catalog","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_READ","Parent":null}},{"Resource":{"Name":"RetrieveCustomer","FullyQualifiedName":"RetrieveCustomer","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customers","FullyQualifiedName":"Customers","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"RetrieveCustomerCustomAttribute","FullyQualifiedName":"RetrieveCustomerCustomAttribute","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Custom Attributes","FullyQualifiedName":"Customer Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"RetrieveCustomerCustomAttributeDefinition","FullyQualifiedName":"RetrieveCustomerCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Custom Attributes","FullyQualifiedName":"Customer Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"RetrieveCustomerGroup","FullyQualifiedName":"RetrieveCustomerGroup","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Groups","FullyQualifiedName":"Customer Groups","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"RetrieveCustomerSegment","FullyQualifiedName":"RetrieveCustomerSegment","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Segments","FullyQualifiedName":"Customer Segments","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"RetrieveDispute","FullyQualifiedName":"RetrieveDispute","Type":"endpoint","Metadata":null,"Parent":{"Name":"Disputes","FullyQualifiedName":"Disputes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"DISPUTES_READ","Parent":null}},{"Resource":{"Name":"RetrieveDisputeEvidence","FullyQualifiedName":"RetrieveDisputeEvidence","Type":"endpoint","Metadata":null,"Parent":{"Name":"Disputes","FullyQualifiedName":"Disputes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"DISPUTES_READ","Parent":null}},{"Resource":{"Name":"RetrieveEmployee (deprecated)","FullyQualifiedName":"RetrieveEmployee (deprecated)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Employees","FullyQualifiedName":"Employees","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"EMPLOYEES_READ","Parent":null}},{"Resource":{"Name":"RetrieveGiftCard","FullyQualifiedName":"RetrieveGiftCard","Type":"endpoint","Metadata":null,"Parent":{"Name":"Gift Cards","FullyQualifiedName":"Gift Cards","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"GIFTCARDS_READ","Parent":null}},{"Resource":{"Name":"RetrieveGiftCardFromGAN","FullyQualifiedName":"RetrieveGiftCardFromGAN","Type":"endpoint","Metadata":null,"Parent":{"Name":"Gift Cards","FullyQualifiedName":"Gift Cards","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"GIFTCARDS_READ","Parent":null}},{"Resource":{"Name":"RetrieveGiftCardFromNonce","FullyQualifiedName":"RetrieveGiftCardFromNonce","Type":"endpoint","Metadata":null,"Parent":{"Name":"Gift Cards","FullyQualifiedName":"Gift Cards","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"GIFTCARDS_READ","Parent":null}},{"Resource":{"Name":"RetrieveInventoryAdjustment","FullyQualifiedName":"RetrieveInventoryAdjustment","Type":"endpoint","Metadata":null,"Parent":{"Name":"Inventory","FullyQualifiedName":"Inventory","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVENTORY_READ","Parent":null}},{"Resource":{"Name":"RetrieveInventoryChanges","FullyQualifiedName":"RetrieveInventoryChanges","Type":"endpoint","Metadata":null,"Parent":{"Name":"Inventory","FullyQualifiedName":"Inventory","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVENTORY_READ","Parent":null}},{"Resource":{"Name":"RetrieveInventoryCount","FullyQualifiedName":"RetrieveInventoryCount","Type":"endpoint","Metadata":null,"Parent":{"Name":"Inventory","FullyQualifiedName":"Inventory","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVENTORY_READ","Parent":null}},{"Resource":{"Name":"RetrieveInventoryPhysicalCount","FullyQualifiedName":"RetrieveInventoryPhysicalCount","Type":"endpoint","Metadata":null,"Parent":{"Name":"Inventory","FullyQualifiedName":"Inventory","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVENTORY_READ","Parent":null}},{"Resource":{"Name":"RetrieveLocation","FullyQualifiedName":"RetrieveLocation","Type":"endpoint","Metadata":null,"Parent":{"Name":"Locations","FullyQualifiedName":"Locations","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_READ","Parent":null}},{"Resource":{"Name":"RetrieveLocationCustomAttribute","FullyQualifiedName":"RetrieveLocationCustomAttribute","Type":"endpoint","Metadata":null,"Parent":{"Name":"Location Custom Attributes","FullyQualifiedName":"Location Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_READ","Parent":null}},{"Resource":{"Name":"RetrieveLocationCustomAttributeDefinition","FullyQualifiedName":"RetrieveLocationCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Location Custom Attributes","FullyQualifiedName":"Location Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_READ","Parent":null}},{"Resource":{"Name":"RetrieveLoyaltyAccount","FullyQualifiedName":"RetrieveLoyaltyAccount","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_READ","Parent":null}},{"Resource":{"Name":"RetrieveLoyaltyProgram","FullyQualifiedName":"RetrieveLoyaltyProgram","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_READ","Parent":null}},{"Resource":{"Name":"RetrieveLoyaltyPromotion","FullyQualifiedName":"RetrieveLoyaltyPromotion","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_READ","Parent":null}},{"Resource":{"Name":"RetrieveLoyaltyReward","FullyQualifiedName":"RetrieveLoyaltyReward","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_READ","Parent":null}},{"Resource":{"Name":"RetrieveMerchant","FullyQualifiedName":"RetrieveMerchant","Type":"endpoint","Metadata":null,"Parent":{"Name":"Merchants","FullyQualifiedName":"Merchants","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_READ","Parent":null}},{"Resource":{"Name":"RetrieveMerchantCustomAttribute","FullyQualifiedName":"RetrieveMerchantCustomAttribute","Type":"endpoint","Metadata":null,"Parent":{"Name":"Merchant Custom Attributes","FullyQualifiedName":"Merchant Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_READ","Parent":null}},{"Resource":{"Name":"RetrieveMerchantCustomAttributeDefinition","FullyQualifiedName":"RetrieveMerchantCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Merchant Custom Attributes","FullyQualifiedName":"Merchant Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_READ","Parent":null}},{"Resource":{"Name":"RetrieveOrder","FullyQualifiedName":"RetrieveOrder","Type":"endpoint","Metadata":null,"Parent":{"Name":"Orders","FullyQualifiedName":"Orders","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_READ","Parent":null}},{"Resource":{"Name":"RetrieveOrder","FullyQualifiedName":"RetrieveOrder","Type":"endpoint","Metadata":null,"Parent":{"Name":"Orders","FullyQualifiedName":"Orders","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"RetrieveOrderCustomAttribute","FullyQualifiedName":"RetrieveOrderCustomAttribute","Type":"endpoint","Metadata":null,"Parent":{"Name":"Order Custom Attributes","FullyQualifiedName":"Order Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_READ","Parent":null}},{"Resource":{"Name":"RetrieveOrderCustomAttributeDefinition","FullyQualifiedName":"RetrieveOrderCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Order Custom Attributes","FullyQualifiedName":"Order Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_READ","Parent":null}},{"Resource":{"Name":"RetrieveSnippet","FullyQualifiedName":"RetrieveSnippet","Type":"endpoint","Metadata":null,"Parent":{"Name":"Snippets","FullyQualifiedName":"Snippets","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ONLINE_STORE_SNIPPETS_READ","Parent":null}},{"Resource":{"Name":"RetrieveSubscription","FullyQualifiedName":"RetrieveSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"SUBSCRIPTIONS_READ","Parent":null}},{"Resource":{"Name":"RetrieveTeamMember","FullyQualifiedName":"RetrieveTeamMember","Type":"endpoint","Metadata":null,"Parent":{"Name":"Team","FullyQualifiedName":"Team","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"EMPLOYEES_READ","Parent":null}},{"Resource":{"Name":"RetrieveTeamMemberBookingProfile","FullyQualifiedName":"RetrieveTeamMemberBookingProfile","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_BUSINESS_SETTINGS_READ","Parent":null}},{"Resource":{"Name":"RetrieveVendor","FullyQualifiedName":"RetrieveVendor","Type":"endpoint","Metadata":null,"Parent":{"Name":"Vendors","FullyQualifiedName":"Vendors","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"VENDOR_READ","Parent":null}},{"Resource":{"Name":"RetrieveWageSetting","FullyQualifiedName":"RetrieveWageSetting","Type":"endpoint","Metadata":null,"Parent":{"Name":"Team","FullyQualifiedName":"Team","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"EMPLOYEES_READ","Parent":null}},{"Resource":{"Name":"SearchAvailability (buyer-level)","FullyQualifiedName":"SearchAvailability (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_READ","Parent":null}},{"Resource":{"Name":"SearchAvailability (seller-level)","FullyQualifiedName":"SearchAvailability (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_READ","Parent":null}},{"Resource":{"Name":"SearchAvailability (seller-level)","FullyQualifiedName":"SearchAvailability (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_READ","Parent":null}},{"Resource":{"Name":"SearchCatalogItems","FullyQualifiedName":"SearchCatalogItems","Type":"endpoint","Metadata":null,"Parent":{"Name":"Catalog","FullyQualifiedName":"Catalog","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_READ","Parent":null}},{"Resource":{"Name":"SearchCatalogObjects","FullyQualifiedName":"SearchCatalogObjects","Type":"endpoint","Metadata":null,"Parent":{"Name":"Catalog","FullyQualifiedName":"Catalog","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_READ","Parent":null}},{"Resource":{"Name":"SearchCustomers","FullyQualifiedName":"SearchCustomers","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customers","FullyQualifiedName":"Customers","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"SearchInvoices","FullyQualifiedName":"SearchInvoices","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVOICES_READ","Parent":null}},{"Resource":{"Name":"SearchLoyaltyAccounts","FullyQualifiedName":"SearchLoyaltyAccounts","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_READ","Parent":null}},{"Resource":{"Name":"SearchLoyaltyEvents","FullyQualifiedName":"SearchLoyaltyEvents","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_READ","Parent":null}},{"Resource":{"Name":"SearchLoyaltyRewards","FullyQualifiedName":"SearchLoyaltyRewards","Type":"endpoint","Metadata":null,"Parent":{"Name":"Loyalty","FullyQualifiedName":"Loyalty","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"LOYALTY_READ","Parent":null}},{"Resource":{"Name":"SearchOrders","FullyQualifiedName":"SearchOrders","Type":"endpoint","Metadata":null,"Parent":{"Name":"Orders","FullyQualifiedName":"Orders","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_READ","Parent":null}},{"Resource":{"Name":"SearchShifts","FullyQualifiedName":"SearchShifts","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"TIMECARDS_READ","Parent":null}},{"Resource":{"Name":"SearchSubscriptions","FullyQualifiedName":"SearchSubscriptions","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"SUBSCRIPTIONS_READ","Parent":null}},{"Resource":{"Name":"SearchTeamMembers","FullyQualifiedName":"SearchTeamMembers","Type":"endpoint","Metadata":null,"Parent":{"Name":"Team","FullyQualifiedName":"Team","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"EMPLOYEES_READ","Parent":null}},{"Resource":{"Name":"SearchTerminalAction","FullyQualifiedName":"SearchTerminalAction","Type":"endpoint","Metadata":null,"Parent":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_READ","Parent":null}},{"Resource":{"Name":"SearchTerminalCheckouts","FullyQualifiedName":"SearchTerminalCheckouts","Type":"endpoint","Metadata":null,"Parent":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_READ","Parent":null}},{"Resource":{"Name":"SearchTerminalRefunds","FullyQualifiedName":"SearchTerminalRefunds","Type":"endpoint","Metadata":null,"Parent":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_READ","Parent":null}},{"Resource":{"Name":"SearchVendors","FullyQualifiedName":"SearchVendors","Type":"endpoint","Metadata":null,"Parent":{"Name":"Vendors","FullyQualifiedName":"Vendors","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"VENDOR_READ","Parent":null}},{"Resource":{"Name":"SubmitEvidence","FullyQualifiedName":"SubmitEvidence","Type":"endpoint","Metadata":null,"Parent":{"Name":"Disputes","FullyQualifiedName":"Disputes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"DISPUTES_WRITE","Parent":null}},{"Resource":{"Name":"SwapPlan","FullyQualifiedName":"SwapPlan","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"SwapPlan","FullyQualifiedName":"SwapPlan","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVOICES_WRITE","Parent":null}},{"Resource":{"Name":"SwapPlan","FullyQualifiedName":"SwapPlan","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_READ","Parent":null}},{"Resource":{"Name":"SwapPlan","FullyQualifiedName":"SwapPlan","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"SwapPlan","FullyQualifiedName":"SwapPlan","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"SwapPlan","FullyQualifiedName":"SwapPlan","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"SUBSCRIPTIONS_WRITE","Parent":null}},{"Resource":{"Name":"UnlinkCustomerFromGiftCard","FullyQualifiedName":"UnlinkCustomerFromGiftCard","Type":"endpoint","Metadata":null,"Parent":{"Name":"Gift Cards","FullyQualifiedName":"Gift Cards","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"GIFTCARDS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateBooking (buyer-level)","FullyQualifiedName":"UpdateBooking (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateBooking (seller-level)","FullyQualifiedName":"UpdateBooking (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_WRITE","Parent":null}},{"Resource":{"Name":"UpdateBooking (seller-level)","FullyQualifiedName":"UpdateBooking (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Bookings","FullyQualifiedName":"Bookings","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateBookingCustomAttributeDefinition (buyer-level)","FullyQualifiedName":"UpdateBookingCustomAttributeDefinition (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateBookingCustomAttributeDefinition (seller-level)","FullyQualifiedName":"UpdateBookingCustomAttributeDefinition (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_WRITE","Parent":null}},{"Resource":{"Name":"UpdateBookingCustomAttributeDefinition (seller-level)","FullyQualifiedName":"UpdateBookingCustomAttributeDefinition (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateBreakType","FullyQualifiedName":"UpdateBreakType","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"TIMECARDS_SETTINGS_READ","Parent":null}},{"Resource":{"Name":"UpdateBreakType","FullyQualifiedName":"UpdateBreakType","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"TIMECARDS_SETTINGS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateCustomer","FullyQualifiedName":"UpdateCustomer","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customers","FullyQualifiedName":"Customers","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateCustomerCustomAttributeDefinition","FullyQualifiedName":"UpdateCustomerCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Custom Attributes","FullyQualifiedName":"Customer Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateCustomerGroup","FullyQualifiedName":"UpdateCustomerGroup","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Groups","FullyQualifiedName":"Customer Groups","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateInvoice","FullyQualifiedName":"UpdateInvoice","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVOICES_WRITE","Parent":null}},{"Resource":{"Name":"UpdateInvoice","FullyQualifiedName":"UpdateInvoice","Type":"endpoint","Metadata":null,"Parent":{"Name":"Invoices","FullyQualifiedName":"Invoices","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateItemModifierLists","FullyQualifiedName":"UpdateItemModifierLists","Type":"endpoint","Metadata":null,"Parent":{"Name":"Catalog","FullyQualifiedName":"Catalog","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateItemTaxes","FullyQualifiedName":"UpdateItemTaxes","Type":"endpoint","Metadata":null,"Parent":{"Name":"Catalog","FullyQualifiedName":"Catalog","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateLocation","FullyQualifiedName":"UpdateLocation","Type":"endpoint","Metadata":null,"Parent":{"Name":"Locations","FullyQualifiedName":"Locations","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"UpdateLocationCustomAttributeDefinition","FullyQualifiedName":"UpdateLocationCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Location Custom Attributes","FullyQualifiedName":"Location Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"UpdateMerchantCustomAttributeDefinition","FullyQualifiedName":"UpdateMerchantCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Merchant Custom Attributes","FullyQualifiedName":"Merchant Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"UpdateOrder","FullyQualifiedName":"UpdateOrder","Type":"endpoint","Metadata":null,"Parent":{"Name":"Orders","FullyQualifiedName":"Orders","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateOrderCustomAttributeDefinition","FullyQualifiedName":"UpdateOrderCustomAttributeDefinition","Type":"endpoint","Metadata":null,"Parent":{"Name":"Order Custom Attributes","FullyQualifiedName":"Order Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateShift","FullyQualifiedName":"UpdateShift","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"TIMECARDS_READ","Parent":null}},{"Resource":{"Name":"UpdateShift","FullyQualifiedName":"UpdateShift","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"TIMECARDS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateSubscription","FullyQualifiedName":"UpdateSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_READ","Parent":null}},{"Resource":{"Name":"UpdateSubscription","FullyQualifiedName":"UpdateSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"INVOICES_WRITE","Parent":null}},{"Resource":{"Name":"UpdateSubscription","FullyQualifiedName":"UpdateSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_READ","Parent":null}},{"Resource":{"Name":"UpdateSubscription","FullyQualifiedName":"UpdateSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateSubscription","FullyQualifiedName":"UpdateSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"PAYMENTS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateSubscription","FullyQualifiedName":"UpdateSubscription","Type":"endpoint","Metadata":null,"Parent":{"Name":"Subscriptions","FullyQualifiedName":"Subscriptions","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"SUBSCRIPTIONS_WRITE","Parent":null}},{"Resource":{"Name":"UpdateTeamMember","FullyQualifiedName":"UpdateTeamMember","Type":"endpoint","Metadata":null,"Parent":{"Name":"Team","FullyQualifiedName":"Team","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"EMPLOYEES_WRITE","Parent":null}},{"Resource":{"Name":"UpdateVendors","FullyQualifiedName":"UpdateVendors","Type":"endpoint","Metadata":null,"Parent":{"Name":"Vendors","FullyQualifiedName":"Vendors","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"VENDOR_WRITE","Parent":null}},{"Resource":{"Name":"UpdateWageSetting","FullyQualifiedName":"UpdateWageSetting","Type":"endpoint","Metadata":null,"Parent":{"Name":"Team","FullyQualifiedName":"Team","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"EMPLOYEES_WRITE","Parent":null}},{"Resource":{"Name":"UpdateWorkweekConfig","FullyQualifiedName":"UpdateWorkweekConfig","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"TIMECARDS_SETTINGS_READ","Parent":null}},{"Resource":{"Name":"UpdateWorkweekConfig","FullyQualifiedName":"UpdateWorkweekConfig","Type":"endpoint","Metadata":null,"Parent":{"Name":"Labor","FullyQualifiedName":"Labor","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"TIMECARDS_SETTINGS_WRITE","Parent":null}},{"Resource":{"Name":"UpsertBookingCustomAttribute (buyer-level)","FullyQualifiedName":"UpsertBookingCustomAttribute (buyer-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"UpsertBookingCustomAttribute (seller-level)","FullyQualifiedName":"UpsertBookingCustomAttribute (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_ALL_WRITE","Parent":null}},{"Resource":{"Name":"UpsertBookingCustomAttribute (seller-level)","FullyQualifiedName":"UpsertBookingCustomAttribute (seller-level)","Type":"endpoint","Metadata":null,"Parent":{"Name":"Booking Custom Attributes","FullyQualifiedName":"Booking Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"APPOINTMENTS_WRITE","Parent":null}},{"Resource":{"Name":"UpsertCatalogObject","FullyQualifiedName":"UpsertCatalogObject","Type":"endpoint","Metadata":null,"Parent":{"Name":"Catalog","FullyQualifiedName":"Catalog","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ITEMS_WRITE","Parent":null}},{"Resource":{"Name":"UpsertCustomerCustomAttribute","FullyQualifiedName":"UpsertCustomerCustomAttribute","Type":"endpoint","Metadata":null,"Parent":{"Name":"Customer Custom Attributes","FullyQualifiedName":"Customer Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"CUSTOMERS_WRITE","Parent":null}},{"Resource":{"Name":"UpsertLocationCustomAttribute","FullyQualifiedName":"UpsertLocationCustomAttribute","Type":"endpoint","Metadata":null,"Parent":{"Name":"Location Custom Attributes","FullyQualifiedName":"Location Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"UpsertMerchantCustomAttribute","FullyQualifiedName":"UpsertMerchantCustomAttribute","Type":"endpoint","Metadata":null,"Parent":{"Name":"Merchant Custom Attributes","FullyQualifiedName":"Merchant Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"MERCHANT_PROFILE_WRITE","Parent":null}},{"Resource":{"Name":"UpsertOrderCustomAttribute","FullyQualifiedName":"UpsertOrderCustomAttribute","Type":"endpoint","Metadata":null,"Parent":{"Name":"Order Custom Attributes","FullyQualifiedName":"Order Custom Attributes","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ORDERS_WRITE","Parent":null}},{"Resource":{"Name":"UpsertSnippet","FullyQualifiedName":"UpsertSnippet","Type":"endpoint","Metadata":null,"Parent":{"Name":"Snippets","FullyQualifiedName":"Snippets","Type":"category","Metadata":null,"Parent":null}},"Permission":{"Value":"ONLINE_STORE_SNIPPETS_WRITE","Parent":null}}],"UnboundedResources":[{"Name":"Truffle Security","FullyQualifiedName":"detectors@trufflesec.com","Type":"team_member","Metadata":{"created_at":"2024-08-19T07:23:17Z","is_owner":true},"Parent":null}],"Metadata":{"client_id":"sq0idp-JqoB3AJCTFtclv4eUkMm_Q","expires_at":"","merchant_id":"ML4DDTXKQNB80"}}
================================================
FILE: pkg/analyzer/analyzers/square/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package square
import "errors"
type Permission int
const (
Invalid Permission = iota
BankAccountsRead Permission = iota
AppointmentsWrite Permission = iota
AppointmentsAllWrite Permission = iota
AppointmentsRead Permission = iota
AppointmentsAllRead Permission = iota
AppointmentsBusinessSettingsRead Permission = iota
PaymentsRead Permission = iota
PaymentsWrite Permission = iota
CashDrawerRead Permission = iota
ItemsWrite Permission = iota
ItemsRead Permission = iota
OrdersWrite Permission = iota
OrdersRead Permission = iota
CustomersWrite Permission = iota
CustomersRead Permission = iota
DeviceCredentialManagement Permission = iota
DevicesRead Permission = iota
DisputesWrite Permission = iota
DisputesRead Permission = iota
EmployeesRead Permission = iota
GiftcardsRead Permission = iota
GiftcardsWrite Permission = iota
InventoryWrite Permission = iota
InventoryRead Permission = iota
InvoicesWrite Permission = iota
InvoicesRead Permission = iota
TimecardsSettingsWrite Permission = iota
TimecardsWrite Permission = iota
TimecardsSettingsRead Permission = iota
TimecardsRead Permission = iota
MerchantProfileWrite Permission = iota
MerchantProfileRead Permission = iota
LoyaltyRead Permission = iota
LoyaltyWrite Permission = iota
PaymentsWriteInPerson Permission = iota
PaymentsWriteSharedOnfile Permission = iota
PaymentsWriteAdditionalRecipients Permission = iota
PayoutsRead Permission = iota
OnlineStoreSiteRead Permission = iota
OnlineStoreSnippetsWrite Permission = iota
OnlineStoreSnippetsRead Permission = iota
SubscriptionsWrite Permission = iota
SubscriptionsRead Permission = iota
)
var (
PermissionStrings = map[Permission]string{
BankAccountsRead: "bank_accounts_read",
AppointmentsWrite: "appointments_write",
AppointmentsAllWrite: "appointments_all_write",
AppointmentsRead: "appointments_read",
AppointmentsAllRead: "appointments_all_read",
AppointmentsBusinessSettingsRead: "appointments_business_settings_read",
PaymentsRead: "payments_read",
PaymentsWrite: "payments_write",
CashDrawerRead: "cash_drawer_read",
ItemsWrite: "items_write",
ItemsRead: "items_read",
OrdersWrite: "orders_write",
OrdersRead: "orders_read",
CustomersWrite: "customers_write",
CustomersRead: "customers_read",
DeviceCredentialManagement: "device_credential_management",
DevicesRead: "devices_read",
DisputesWrite: "disputes_write",
DisputesRead: "disputes_read",
EmployeesRead: "employees_read",
GiftcardsRead: "giftcards_read",
GiftcardsWrite: "giftcards_write",
InventoryWrite: "inventory_write",
InventoryRead: "inventory_read",
InvoicesWrite: "invoices_write",
InvoicesRead: "invoices_read",
TimecardsSettingsWrite: "timecards_settings_write",
TimecardsWrite: "timecards_write",
TimecardsSettingsRead: "timecards_settings_read",
TimecardsRead: "timecards_read",
MerchantProfileWrite: "merchant_profile_write",
MerchantProfileRead: "merchant_profile_read",
LoyaltyRead: "loyalty_read",
LoyaltyWrite: "loyalty_write",
PaymentsWriteInPerson: "payments_write_in_person",
PaymentsWriteSharedOnfile: "payments_write_shared_onfile",
PaymentsWriteAdditionalRecipients: "payments_write_additional_recipients",
PayoutsRead: "payouts_read",
OnlineStoreSiteRead: "online_store_site_read",
OnlineStoreSnippetsWrite: "online_store_snippets_write",
OnlineStoreSnippetsRead: "online_store_snippets_read",
SubscriptionsWrite: "subscriptions_write",
SubscriptionsRead: "subscriptions_read",
}
StringToPermission = map[string]Permission{
"bank_accounts_read": BankAccountsRead,
"appointments_write": AppointmentsWrite,
"appointments_all_write": AppointmentsAllWrite,
"appointments_read": AppointmentsRead,
"appointments_all_read": AppointmentsAllRead,
"appointments_business_settings_read": AppointmentsBusinessSettingsRead,
"payments_read": PaymentsRead,
"payments_write": PaymentsWrite,
"cash_drawer_read": CashDrawerRead,
"items_write": ItemsWrite,
"items_read": ItemsRead,
"orders_write": OrdersWrite,
"orders_read": OrdersRead,
"customers_write": CustomersWrite,
"customers_read": CustomersRead,
"device_credential_management": DeviceCredentialManagement,
"devices_read": DevicesRead,
"disputes_write": DisputesWrite,
"disputes_read": DisputesRead,
"employees_read": EmployeesRead,
"giftcards_read": GiftcardsRead,
"giftcards_write": GiftcardsWrite,
"inventory_write": InventoryWrite,
"inventory_read": InventoryRead,
"invoices_write": InvoicesWrite,
"invoices_read": InvoicesRead,
"timecards_settings_write": TimecardsSettingsWrite,
"timecards_write": TimecardsWrite,
"timecards_settings_read": TimecardsSettingsRead,
"timecards_read": TimecardsRead,
"merchant_profile_write": MerchantProfileWrite,
"merchant_profile_read": MerchantProfileRead,
"loyalty_read": LoyaltyRead,
"loyalty_write": LoyaltyWrite,
"payments_write_in_person": PaymentsWriteInPerson,
"payments_write_shared_onfile": PaymentsWriteSharedOnfile,
"payments_write_additional_recipients": PaymentsWriteAdditionalRecipients,
"payouts_read": PayoutsRead,
"online_store_site_read": OnlineStoreSiteRead,
"online_store_snippets_write": OnlineStoreSnippetsWrite,
"online_store_snippets_read": OnlineStoreSnippetsRead,
"subscriptions_write": SubscriptionsWrite,
"subscriptions_read": SubscriptionsRead,
}
PermissionIDs = map[Permission]int{
BankAccountsRead: 1,
AppointmentsWrite: 2,
AppointmentsAllWrite: 3,
AppointmentsRead: 4,
AppointmentsAllRead: 5,
AppointmentsBusinessSettingsRead: 6,
PaymentsRead: 7,
PaymentsWrite: 8,
CashDrawerRead: 9,
ItemsWrite: 10,
ItemsRead: 11,
OrdersWrite: 12,
OrdersRead: 13,
CustomersWrite: 14,
CustomersRead: 15,
DeviceCredentialManagement: 16,
DevicesRead: 17,
DisputesWrite: 18,
DisputesRead: 19,
EmployeesRead: 20,
GiftcardsRead: 21,
GiftcardsWrite: 22,
InventoryWrite: 23,
InventoryRead: 24,
InvoicesWrite: 25,
InvoicesRead: 26,
TimecardsSettingsWrite: 27,
TimecardsWrite: 28,
TimecardsSettingsRead: 29,
TimecardsRead: 30,
MerchantProfileWrite: 31,
MerchantProfileRead: 32,
LoyaltyRead: 33,
LoyaltyWrite: 34,
PaymentsWriteInPerson: 35,
PaymentsWriteSharedOnfile: 36,
PaymentsWriteAdditionalRecipients: 37,
PayoutsRead: 38,
OnlineStoreSiteRead: 39,
OnlineStoreSnippetsWrite: 40,
OnlineStoreSnippetsRead: 41,
SubscriptionsWrite: 42,
SubscriptionsRead: 43,
}
IdToPermission = map[int]Permission{
1: BankAccountsRead,
2: AppointmentsWrite,
3: AppointmentsAllWrite,
4: AppointmentsRead,
5: AppointmentsAllRead,
6: AppointmentsBusinessSettingsRead,
7: PaymentsRead,
8: PaymentsWrite,
9: CashDrawerRead,
10: ItemsWrite,
11: ItemsRead,
12: OrdersWrite,
13: OrdersRead,
14: CustomersWrite,
15: CustomersRead,
16: DeviceCredentialManagement,
17: DevicesRead,
18: DisputesWrite,
19: DisputesRead,
20: EmployeesRead,
21: GiftcardsRead,
22: GiftcardsWrite,
23: InventoryWrite,
24: InventoryRead,
25: InvoicesWrite,
26: InvoicesRead,
27: TimecardsSettingsWrite,
28: TimecardsWrite,
29: TimecardsSettingsRead,
30: TimecardsRead,
31: MerchantProfileWrite,
32: MerchantProfileRead,
33: LoyaltyRead,
34: LoyaltyWrite,
35: PaymentsWriteInPerson,
36: PaymentsWriteSharedOnfile,
37: PaymentsWriteAdditionalRecipients,
38: PayoutsRead,
39: OnlineStoreSiteRead,
40: OnlineStoreSnippetsWrite,
41: OnlineStoreSnippetsRead,
42: SubscriptionsWrite,
43: SubscriptionsRead,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/square/permissions.yaml
================================================
permissions:
- bank_accounts_read
- appointments_write
- appointments_all_write
- appointments_read
- appointments_all_read
- appointments_business_settings_read
- payments_read
- payments_write
- cash_drawer_read
- items_write
- items_read
- orders_write
- orders_read
- customers_write
- customers_read
- device_credential_management
- devices_read
- disputes_write
- disputes_read
- employees_read
- giftcards_read
- giftcards_write
- inventory_write
- inventory_read
- invoices_write
- invoices_read
- timecards_settings_write
- timecards_write
- timecards_settings_read
- timecards_read
- merchant_profile_write
- merchant_profile_read
- loyalty_read
- loyalty_write
- payments_write_in_person
- payments_write_shared_onfile
- payments_write_additional_recipients
- payouts_read
- online_store_site_read
- online_store_snippets_write
- online_store_snippets_read
- subscriptions_write
- subscriptions_read
================================================
FILE: pkg/analyzer/analyzers/square/scopes.go
================================================
package square
var permissions_slice = []map[string]map[string][]string{
{
"Bank Accounts": {
"GetBankAccount": []string{"BANK_ACCOUNTS_READ"},
"ListBankAccounts": []string{"BANK_ACCOUNTS_READ"},
"GetBankAccountByV1Id": []string{"BANK_ACCOUNTS_READ"},
},
},
{
"Bookings": {
"CreateBooking (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"CreateBooking (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
"SearchAvailability (buyer-level)": []string{"APPOINTMENTS_READ"},
"SearchAvailability (seller-level)": []string{"APPOINTMENTS_READ", "APPOINTMENTS_ALL_READ"},
"RetrieveBusinessBookingProfile": []string{"APPOINTMENTS_BUSINESS_SETTINGS_READ"},
"ListTeamMemberBookingProfiles": []string{"APPOINTMENTS_BUSINESS_SETTINGS_READ"},
"RetrieveTeamMemberBookingProfile": []string{"APPOINTMENTS_BUSINESS_SETTINGS_READ"},
"ListBookings (buyer-level)": []string{"APPOINTMENTS_READ"},
"ListBookings (seller-level)": []string{"APPOINTMENTS_READ", "APPOINTMENTS_ALL_READ"},
"RetrieveBooking (buyer-level)": []string{"APPOINTMENTS_READ"},
"RetrieveBooking (seller-level)": []string{"APPOINTMENTS_READ", "APPOINTMENTS_ALL_READ"},
"UpdateBooking (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"UpdateBooking (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
"CancelBooking (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"CancelBooking (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
},
},
{
"Booking Custom Attributes": {
"CreateBookingCustomAttributeDefinition (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"CreateBookingCustomAttributeDefinition (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
"UpdateBookingCustomAttributeDefinition (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"UpdateBookingCustomAttributeDefinition (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
"ListBookingCustomAttributeDefinitions (buyer-level)": []string{"APPOINTMENTS_READ"},
"ListBookingCustomAttributeDefinitions (seller-level)": []string{"APPOINTMENTS_READ", "APPOINTMENTS_ALL_READ"},
"RetrieveBookingCustomAttributeDefinition (buyer-level)": []string{"APPOINTMENTS_READ"},
"RetrieveBookingCustomAttributeDefinition (seller-level)": []string{"APPOINTMENTS_READ", "APPOINTMENTS_ALL_READ"},
"DeleteBookingCustomAttributeDefinition (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"DeleteBookingCustomAttributeDefinition (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
"UpsertBookingCustomAttribute (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"UpsertBookingCustomAttribute (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
"BulkUpsertBookingCustomAttributes (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"BulkUpsertBookingCustomAttributes (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
"ListBookingCustomAttributes (buyer-level)": []string{"APPOINTMENTS_READ"},
"ListBookingCustomAttributes (seller-level)": []string{"APPOINTMENTS_READ", "APPOINTMENTS_ALL_READ"},
"RetrieveBookingCustomAttribute (buyer-level)": []string{"APPOINTMENTS_READ"},
"RetrieveBookingCustomAttribute (seller-level)": []string{"APPOINTMENTS_READ", "APPOINTMENTS_ALL_READ"},
"DeleteBookingCustomAttribute (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"DeleteBookingCustomAttribute (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
},
},
{
"Cards": {
"ListCards": []string{"PAYMENTS_READ"},
"CreateCard": []string{"PAYMENTS_WRITE"},
"RetrieveCard": []string{"PAYMENTS_READ"},
"DisableCard": []string{"PAYMENTS_WRITE"},
},
},
{
"Cash Drawer Shifts": {
"ListCashDrawerShifts": []string{"CASH_DRAWER_READ"},
"ListCashDrawerShiftEvents": []string{"CASH_DRAWER_READ"},
"RetrieveCashDrawerShift": []string{"CASH_DRAWER_READ"},
},
},
{
"Catalog": {
"BatchDeleteCatalogObjects": []string{"ITEMS_WRITE"},
"BatchUpsertCatalogObjects": []string{"ITEMS_WRITE"},
"BatchRetrieveCatalogObjects": []string{"ITEMS_READ"},
"CatalogInfo": []string{"ITEMS_READ"},
"CreateCatalogImage": []string{"ITEMS_WRITE"},
"DeleteCatalogObject": []string{"ITEMS_WRITE"},
"ListCatalog": []string{"ITEMS_READ"},
"RetrieveCatalogObject": []string{"ITEMS_READ"},
"SearchCatalogItems": []string{"ITEMS_READ"},
"SearchCatalogObjects": []string{"ITEMS_READ"},
"UpdateItemTaxes": []string{"ITEMS_WRITE"},
"UpdateItemModifierLists": []string{"ITEMS_WRITE"},
"UpsertCatalogObject": []string{"ITEMS_WRITE"},
},
},
{
"Checkout": {
"CreatePaymentLink": []string{"ORDERS_WRITE", "ORDERS_READ", "PAYMENTS_WRITE"},
},
},
{
"Customers": {
"AddGroupToCustomer": []string{"CUSTOMERS_WRITE"},
"BulkCreateCustomers": []string{"CUSTOMERS_WRITE"},
"BulkDeleteCustomers": []string{"CUSTOMERS_WRITE"},
"BulkRetrieveCustomers": []string{"CUSTOMERS_READ"},
"BulkUpdateCustomers": []string{"CUSTOMERS_WRITE"},
"CreateCustomer": []string{"CUSTOMERS_WRITE"},
"CreateCustomerCard (deprecated)": []string{"CUSTOMERS_WRITE"},
"DeleteCustomer": []string{"CUSTOMERS_WRITE"},
"DeleteCustomerCard (deprecated)": []string{"CUSTOMERS_WRITE"},
"ListCustomers": []string{"CUSTOMERS_READ"},
"RemoveGroupFromCustomer": []string{"CUSTOMERS_WRITE"},
"RetrieveCustomer": []string{"CUSTOMERS_READ"},
"SearchCustomers": []string{"CUSTOMERS_READ"},
"UpdateCustomer": []string{"CUSTOMERS_WRITE"},
},
},
{
"Customer Custom Attributes": {
"CreateCustomerCustomAttributeDefinition": []string{"CUSTOMERS_WRITE"},
"UpdateCustomerCustomAttributeDefinition": []string{"CUSTOMERS_WRITE"},
"ListCustomerCustomAttributeDefinitions": []string{"CUSTOMERS_READ"},
"RetrieveCustomerCustomAttributeDefinition": []string{"CUSTOMERS_READ"},
"DeleteCustomerCustomAttributeDefinition": []string{"CUSTOMERS_WRITE"},
"UpsertCustomerCustomAttribute": []string{"CUSTOMERS_WRITE"},
"BulkUpsertCustomerCustomAttributes": []string{"CUSTOMERS_WRITE"},
"ListCustomerCustomAttributes": []string{"CUSTOMERS_READ"},
"RetrieveCustomerCustomAttribute": []string{"CUSTOMERS_READ"},
"DeleteCustomerCustomAttribute": []string{"CUSTOMERS_WRITE"},
},
},
{
"Customer Groups": {
"CreateCustomerGroup": []string{"CUSTOMERS_WRITE"},
"DeleteCustomerGroup": []string{"CUSTOMERS_WRITE"},
"ListCustomerGroups": []string{"CUSTOMERS_READ"},
"RetrieveCustomerGroup": []string{"CUSTOMERS_READ"},
"UpdateCustomerGroup": []string{"CUSTOMERS_WRITE"},
},
},
{
"Customer Segments": {
"ListCustomerSegments": []string{"CUSTOMERS_READ"},
"RetrieveCustomerSegment": []string{"CUSTOMERS_READ"},
},
},
{
"Devices": {
"CreateDeviceCode": []string{"DEVICE_CREDENTIAL_MANAGEMENT"},
"GetDeviceCode": []string{"DEVICE_CREDENTIAL_MANAGEMENT"},
"ListDeviceCodes": []string{"DEVICE_CREDENTIAL_MANAGEMENT"},
"ListDevices": []string{"DEVICES_READ"},
"GetDevice": []string{"DEVICES_READ"},
},
},
{
"Disputes": {
"AcceptDispute": []string{"DISPUTES_WRITE"},
"CreateDisputeEvidenceFile": []string{"DISPUTES_WRITE"},
"CreateDisputeEvidenceText": []string{"DISPUTES_WRITE"},
"ListDisputeEvidence": []string{"DISPUTES_READ"},
"ListDisputes": []string{"DISPUTES_READ"},
"DeleteDisputeEvidence": []string{"DISPUTES_WRITE"},
"RetrieveDispute": []string{"DISPUTES_READ"},
"RetrieveDisputeEvidence": []string{"DISPUTES_READ"},
"SubmitEvidence": []string{"DISPUTES_WRITE"},
},
},
{
"Employees": {
"ListEmployees (deprecated)": []string{"EMPLOYEES_READ"},
"RetrieveEmployee (deprecated)": []string{"EMPLOYEES_READ"},
},
},
{
"Gift Cards": {
"ListGiftCards": []string{"GIFTCARDS_READ"},
"CreateGiftCard": []string{"GIFTCARDS_WRITE"},
"RetrieveGiftCard": []string{"GIFTCARDS_READ"},
"RetrieveGiftCardFromGAN": []string{"GIFTCARDS_READ"},
"RetrieveGiftCardFromNonce": []string{"GIFTCARDS_READ"},
"LinkCustomerToGiftCard": []string{"GIFTCARDS_WRITE"},
"UnlinkCustomerFromGiftCard": []string{"GIFTCARDS_WRITE"},
},
},
{
"Gift Card Activities": {
"ListGiftCardActivities": []string{"GIFTCARDS_READ"},
"CreateGiftCardActivity": []string{"GIFTCARDS_WRITE"},
},
},
{
"Inventory": {
"BatchChangeInventory": []string{"INVENTORY_WRITE"},
"BatchRetrieveInventoryCounts": []string{"INVENTORY_READ"},
"BatchRetrieveInventoryChanges": []string{"INVENTORY_READ"},
"RetrieveInventoryAdjustment": []string{"INVENTORY_READ"},
"RetrieveInventoryChanges": []string{"INVENTORY_READ"},
"RetrieveInventoryCount": []string{"INVENTORY_READ"},
"RetrieveInventoryPhysicalCount": []string{"INVENTORY_READ"},
},
},
{
"Invoices": {
"CreateInvoice": []string{"INVOICES_WRITE", "ORDERS_WRITE"},
"PublishInvoice": []string{"CUSTOMERS_READ", "PAYMENTS_WRITE", "INVOICES_WRITE", "ORDERS_WRITE"},
"GetInvoice": []string{"INVOICES_READ"},
"ListInvoices": []string{"INVOICES_READ"},
"SearchInvoices": []string{"INVOICES_READ"},
"CreateInvoiceAttachment": []string{"INVOICES_WRITE", "ORDERS_WRITE"},
"DeleteInvoiceAttachment": []string{"INVOICES_WRITE", "ORDERS_WRITE"},
"UpdateInvoice": []string{"INVOICES_WRITE", "ORDERS_WRITE"},
"DeleteInvoice": []string{"INVOICES_WRITE", "ORDERS_WRITE"},
"CancelInvoice": []string{"INVOICES_WRITE", "ORDERS_WRITE"},
},
},
{
"Labor": {
"CreateBreakType": []string{"TIMECARDS_SETTINGS_WRITE"},
"CreateShift": []string{"TIMECARDS_WRITE"},
"DeleteBreakType": []string{"TIMECARDS_SETTINGS_WRITE"},
"DeleteShift": []string{"TIMECARDS_WRITE"},
"GetBreakType": []string{"TIMECARDS_SETTINGS_READ"},
"GetTeamMemberWage": []string{"EMPLOYEES_READ"},
"GetShift": []string{"TIMECARDS_READ"},
"ListBreakTypes": []string{"TIMECARDS_SETTINGS_READ"},
"ListTeamMemberWages": []string{"EMPLOYEES_READ"},
"ListWorkweekConfigs": []string{"TIMECARDS_SETTINGS_READ"},
"SearchShifts": []string{"TIMECARDS_READ"},
"UpdateShift": []string{"TIMECARDS_WRITE", "TIMECARDS_READ"},
"UpdateWorkweekConfig": []string{"TIMECARDS_SETTINGS_WRITE", "TIMECARDS_SETTINGS_READ"},
"UpdateBreakType": []string{"TIMECARDS_SETTINGS_WRITE", "TIMECARDS_SETTINGS_READ"},
},
},
{
"Locations": {
"CreateLocation": []string{"MERCHANT_PROFILE_WRITE"},
"ListLocations": []string{"MERCHANT_PROFILE_READ"},
"RetrieveLocation": []string{"MERCHANT_PROFILE_READ"},
"UpdateLocation": []string{"MERCHANT_PROFILE_WRITE"},
},
},
{
"Location Custom Attributes": {
"CreateLocationCustomAttributeDefinition": []string{"MERCHANT_PROFILE_WRITE"},
"UpdateLocationCustomAttributeDefinition": []string{"MERCHANT_PROFILE_WRITE"},
"ListLocationCustomAttributeDefinitions": []string{"MERCHANT_PROFILE_READ"},
"RetrieveLocationCustomAttributeDefinition": []string{"MERCHANT_PROFILE_READ"},
"DeleteLocationCustomAttributeDefinition": []string{"MERCHANT_PROFILE_WRITE"},
"UpsertLocationCustomAttribute": []string{"MERCHANT_PROFILE_WRITE"},
"BulkUpsertLocationCustomAttributes": []string{"MERCHANT_PROFILE_WRITE"},
"ListLocationCustomAttributes": []string{"MERCHANT_PROFILE_READ"},
"RetrieveLocationCustomAttribute": []string{"MERCHANT_PROFILE_READ"},
"DeleteLocationCustomAttribute": []string{"MERCHANT_PROFILE_WRITE"},
"BulkDeleteLocationCustomAttributes": []string{"MERCHANT_PROFILE_WRITE"},
},
},
{
"Loyalty": {
"RetrieveLoyaltyProgram": []string{"LOYALTY_READ"},
"ListLoyaltyPrograms (deprecated)": []string{"LOYALTY_READ"},
"CreateLoyaltyPromotion": []string{"LOYALTY_WRITE"},
"ListLoyaltyPromotions": []string{"LOYALTY_READ"},
"RetrieveLoyaltyPromotion": []string{"LOYALTY_READ"},
"CancelLoyaltyPromotion": []string{"LOYALTY_WRITE"},
"CreateLoyaltyAccount": []string{"LOYALTY_WRITE"},
"RetrieveLoyaltyAccount": []string{"LOYALTY_READ"},
"SearchLoyaltyAccounts": []string{"LOYALTY_READ"},
"AccumulateLoyaltyPoints": []string{"LOYALTY_WRITE"},
"AdjustLoyaltyPoints": []string{"LOYALTY_WRITE"},
"CalculateLoyaltyPoints": []string{"LOYALTY_READ"},
"CreateLoyaltyReward": []string{"LOYALTY_WRITE"},
"RedeemLoyaltyReward": []string{"LOYALTY_WRITE"},
"RetrieveLoyaltyReward": []string{"LOYALTY_READ"},
"SearchLoyaltyRewards": []string{"LOYALTY_READ"},
"DeleteLoyaltyReward": []string{"LOYALTY_WRITE"},
"SearchLoyaltyEvents": []string{"LOYALTY_READ"},
},
},
{
"Merchants": {
"ListMerchants": []string{"MERCHANT_PROFILE_READ"},
"RetrieveMerchant": []string{"MERCHANT_PROFILE_READ"},
},
},
{
"Merchant Custom Attributes": {
"CreateMerchantCustomAttributeDefinition": []string{"MERCHANT_PROFILE_WRITE"},
"UpdateMerchantCustomAttributeDefinition": []string{"MERCHANT_PROFILE_WRITE"},
"ListMerchantCustomAttributeDefinitions": []string{"MERCHANT_PROFILE_READ"},
"RetrieveMerchantCustomAttributeDefinition": []string{"MERCHANT_PROFILE_READ"},
"DeleteMerchantCustomAttributeDefinition": []string{"MERCHANT_PROFILE_WRITE"},
"UpsertMerchantCustomAttribute": []string{"MERCHANT_PROFILE_WRITE"},
"BulkUpsertMerchantCustomAttributes": []string{"MERCHANT_PROFILE_WRITE"},
"ListMerchantCustomAttributes": []string{"MERCHANT_PROFILE_READ"},
"RetrieveMerchantCustomAttribute": []string{"MERCHANT_PROFILE_READ"},
"DeleteMerchantCustomAttribute": []string{"MERCHANT_PROFILE_WRITE"},
"BulkDeleteMerchantCustomAttributes": []string{"MERCHANT_PROFILE_WRITE"},
},
},
{
"Mobile Authorization": {
"CreateMobileAuthorizationCode": []string{"PAYMENTS_WRITE_IN_PERSON"},
},
},
{
"Orders": {
"CloneOrder": []string{"ORDERS_WRITE"},
"CreateOrder": []string{"ORDERS_WRITE"},
"BatchRetrieveOrders": []string{"ORDERS_READ"},
"PayOrder": []string{"ORDERS_WRITE", "PAYMENTS_WRITE"},
"RetrieveOrder": []string{"ORDERS_WRITE", "ORDERS_READ"},
"SearchOrders": []string{"ORDERS_READ"},
"UpdateOrder": []string{"ORDERS_WRITE"},
},
},
{
"Order Custom Attributes": {
"CreateOrderCustomAttributeDefinition": []string{"ORDERS_WRITE"},
"UpdateOrderCustomAttributeDefinition": []string{"ORDERS_WRITE"},
"ListOrderCustomAttributeDefinitions": []string{"ORDERS_READ"},
"RetrieveOrderCustomAttributeDefinition": []string{"ORDERS_READ"},
"DeleteOrderCustomAttributeDefinition": []string{"ORDERS_WRITE"},
"UpsertOrderCustomAttribute": []string{"ORDERS_WRITE"},
"BulkUpsertOrderCustomAttributes": []string{"ORDERS_WRITE"},
"ListOrderCustomAttributes": []string{"ORDERS_READ"},
"RetrieveOrderCustomAttribute": []string{"ORDERS_READ"},
"DeleteOrderCustomAttribute": []string{"ORDERS_WRITE"},
"BulkDeleteOrderCustomAttributes": []string{"ORDERS_WRITE"},
},
},
{
"Payments and Refunds": {
"CancelPayment": []string{"PAYMENTS_WRITE"},
"CancelPaymentByIdempotencyKey": []string{"PAYMENTS_WRITE"},
"CompletePayment": []string{"PAYMENTS_WRITE"},
"CreatePayment": []string{"PAYMENTS_WRITE", "PAYMENTS_WRITE_SHARED_ONFILE", "PAYMENTS_WRITE_ADDITIONAL_RECIPIENTS"},
"GetPayment": []string{"PAYMENTS_READ"},
"GetPaymentRefund": []string{"PAYMENTS_READ"},
"ListPayments": []string{"PAYMENTS_READ"},
"ListPaymentRefunds": []string{"PAYMENTS_READ"},
"RefundPayment": []string{"PAYMENTS_WRITE", "PAYMENTS_WRITE_ADDITIONAL_RECIPIENTS"},
},
},
{
"Payouts": {
"ListPayouts": []string{"PAYOUTS_READ"},
"GetPayout": []string{"PAYOUTS_READ"},
"ListPayoutEntries": []string{"PAYOUTS_READ"},
},
},
{
"Sites": {
"ListSites": []string{"ONLINE_STORE_SITE_READ"},
},
},
{
"Snippets": {
"UpsertSnippet": []string{"ONLINE_STORE_SNIPPETS_WRITE"},
"RetrieveSnippet": []string{"ONLINE_STORE_SNIPPETS_READ"},
"DeleteSnippet": []string{"ONLINE_STORE_SNIPPETS_WRITE"},
},
},
{
"Subscriptions": {
"CreateSubscription": []string{"CUSTOMERS_READ", "PAYMENTS_WRITE", "SUBSCRIPTIONS_WRITE", "ITEMS_READ", "ORDERS_WRITE", "INVOICES_WRITE"},
"SearchSubscriptions": []string{"SUBSCRIPTIONS_READ"},
"RetrieveSubscription": []string{"SUBSCRIPTIONS_READ"},
"UpdateSubscription": []string{"CUSTOMERS_READ", "PAYMENTS_WRITE", "SUBSCRIPTIONS_WRITE", "ITEMS_READ", "ORDERS_WRITE", "INVOICES_WRITE"},
"CancelSubscription": []string{"SUBSCRIPTIONS_WRITE"},
"ListSubscriptionEvents": []string{"SUBSCRIPTIONS_READ"},
"ResumeSubscription": []string{"CUSTOMERS_READ", "PAYMENTS_WRITE", "SUBSCRIPTIONS_WRITE", "ITEMS_READ", "ORDERS_WRITE", "INVOICES_WRITE"},
"PauseSubscription": []string{"CUSTOMERS_READ", "PAYMENTS_WRITE", "SUBSCRIPTIONS_WRITE", "ITEMS_READ", "ORDERS_WRITE", "INVOICES_WRITE"},
"SwapPlan": []string{"CUSTOMERS_READ", "PAYMENTS_WRITE", "SUBSCRIPTIONS_WRITE", "ITEMS_READ", "ORDERS_WRITE", "INVOICES_WRITE"},
"DeleteSubscriptionAction": []string{"SUBSCRIPTIONS_WRITE"},
},
},
{
"Team": {
"BulkCreateTeamMembers": []string{"EMPLOYEES_WRITE"},
"BulkUpdateTeamMembers": []string{"EMPLOYEES_WRITE"},
"CreateTeamMember": []string{"EMPLOYEES_WRITE"},
"UpdateTeamMember": []string{"EMPLOYEES_WRITE"},
"RetrieveTeamMember": []string{"EMPLOYEES_READ"},
"RetrieveWageSetting": []string{"EMPLOYEES_READ"},
"SearchTeamMembers": []string{"EMPLOYEES_READ"},
"UpdateWageSetting": []string{"EMPLOYEES_WRITE"},
},
},
{
"Terminal": {
"CreateTerminalCheckout": []string{"PAYMENTS_WRITE"},
"CancelTerminalCheckout": []string{"PAYMENTS_WRITE"},
"GetTerminalCheckout": []string{"PAYMENTS_READ"},
"SearchTerminalCheckouts": []string{"PAYMENTS_READ"},
"CreateTerminalRefund": []string{"PAYMENTS_WRITE"},
"CancelTerminalRefund": []string{"PAYMENTS_WRITE"},
"GetTerminalRefund": []string{"PAYMENTS_READ"},
"SearchTerminalRefunds": []string{"PAYMENTS_READ"},
"CreateTerminalAction": []string{"PAYMENTS_WRITE"},
"CancelTerminalAction": []string{"PAYMENTS_WRITE"},
"GetTerminalAction": []string{"PAYMENTS_READ", "CUSTOMERS_READ"},
"SearchTerminalAction": []string{"PAYMENTS_READ"},
},
},
{
"Vendors": {
"BulkCreateVendors": []string{"VENDOR_WRITE"},
"BulkRetrieveVendors": []string{"VENDOR_READ"},
"BulkUpdateVendors": []string{"VENDOR_WRITE"},
"CreateVendor": []string{"VENDOR_WRITE"},
"SearchVendors": []string{"VENDOR_READ"},
"RetrieveVendor": []string{"VENDOR_READ"},
"UpdateVendors": []string{"VENDOR_WRITE"},
},
},
}
================================================
FILE: pkg/analyzer/analyzers/square/square.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go square
package square
import (
"encoding/json"
"errors"
"net/http"
"os"
"strconv"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeSquare }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, errors.New("key not found in credentialInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeSquare,
UnboundedResources: []analyzers.Resource{},
Metadata: map[string]any{
"expires_at": info.Permissions.ExpiresAt,
"client_id": info.Permissions.ClientID,
"merchant_id": info.Permissions.MerchantID,
},
}
bindings, unboundedResources := getBindingsAndUnboundedResources(info.Permissions.Scopes)
result.Bindings = bindings
result.UnboundedResources = append(result.UnboundedResources, unboundedResources...)
result.UnboundedResources = append(result.UnboundedResources, getTeamMembersResources(info.Team)...)
return &result
}
// Convert given list of team members into resources
func getTeamMembersResources(team TeamJSON) []analyzers.Resource {
teamMembersResources := make([]analyzers.Resource, len(team.TeamMembers))
for idx, teamMember := range team.TeamMembers {
teamMembersResources[idx] = analyzers.Resource{
Name: teamMember.FirstName + " " + teamMember.LastName,
FullyQualifiedName: teamMember.Email,
Type: "team_member",
Metadata: map[string]any{
"is_owner": teamMember.IsOwner,
"created_at": teamMember.CreatedAt,
},
}
}
return teamMembersResources
}
// Build a list of Bindings and UnboundedResources by referencing the category permissions list and
// checking with the given scopes
func getBindingsAndUnboundedResources(scopes []string) ([]analyzers.Binding, []analyzers.Resource) {
bindings := []analyzers.Binding{}
unboundedResources := []analyzers.Resource{}
for _, permissions_category := range permissions_slice {
for category, permissions := range permissions_category {
parentResource := analyzers.Resource{
Name: category,
FullyQualifiedName: category,
Type: "category",
Metadata: nil,
Parent: nil,
}
categoryBinding := make([]analyzers.Binding, 0)
for endpoint, requiredPermissions := range permissions {
resource := analyzers.Resource{
Name: endpoint,
FullyQualifiedName: endpoint,
Type: "endpoint",
Metadata: nil,
Parent: &parentResource,
}
for _, permission := range requiredPermissions {
if _, ok := StringToPermission[permission]; !ok { // skip unknown permissions
continue
}
if contains(scopes, permission) {
categoryBinding = append(categoryBinding, analyzers.Binding{
Resource: resource,
Permission: analyzers.Permission{
Value: permission,
},
})
}
}
}
if len(categoryBinding) == 0 {
unboundedResources = append(unboundedResources, parentResource)
} else {
bindings = append(bindings, categoryBinding...)
}
}
}
return bindings, unboundedResources
}
type TeamJSON struct {
TeamMembers []struct {
IsOwner bool `json:"is_owner"`
FirstName string `json:"given_name"`
LastName string `json:"family_name"`
Email string `json:"email_address"`
CreatedAt string `json:"created_at"`
} `json:"team_members"`
}
type PermissionsJSON struct {
Scopes []string `json:"scopes"`
ExpiresAt string `json:"expires_at"`
ClientID string `json:"client_id"`
MerchantID string `json:"merchant_id"`
}
type SecretInfo struct {
Permissions PermissionsJSON
Team TeamJSON
}
func getPermissions(cfg *config.Config, key string) (PermissionsJSON, error) {
var permissions PermissionsJSON
// POST request is considered as non-safe. Square Post request does not change any state.
// We are using unrestricted client to avoid error for non-safe API request.
client := analyzers.NewAnalyzeClientUnrestricted(cfg)
req, err := http.NewRequest("POST", "https://connect.squareup.com/oauth2/token/status", nil)
if err != nil {
return permissions, err
}
req.Header.Add("Authorization", "Bearer "+key)
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Square-Version", "2024-06-04")
resp, err := client.Do(req)
if err != nil {
return permissions, err
}
if resp.StatusCode != 200 {
return permissions, nil
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&permissions)
if err != nil {
return permissions, err
}
return permissions, nil
}
func getUsers(cfg *config.Config, key string) (TeamJSON, error) {
var team TeamJSON
// POST request is considered as non-safe. Square Post request does not change any state.
// We are using unrestricted client to avoid error for non-safe API request.
client := analyzers.NewAnalyzeClientUnrestricted(cfg)
req, err := http.NewRequest("POST", "https://connect.squareup.com/v2/team-members/search", nil)
if err != nil {
return team, err
}
req.Header.Add("Authorization", "Bearer "+key)
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Square-Version", "2024-06-04")
q := req.URL.Query()
q.Add("limit", "200")
req.URL.RawQuery = q.Encode()
resp, err := client.Do(req)
if err != nil {
return team, err
}
if resp.StatusCode != 200 {
return team, nil
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&team)
if err != nil {
return team, err
}
return team, nil
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
permissions, err := getPermissions(cfg, key)
if err != nil {
return nil, err
}
team, err := getUsers(cfg, key)
if err != nil {
return nil, err
}
return &SecretInfo{
Permissions: permissions,
Team: team,
}, nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
// ToDo: Add in logging
if cfg.LoggingEnabled {
color.Red("[x] Logging is not supported for this analyzer.")
return
}
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
if info.Permissions.MerchantID == "" {
color.Red("[x] Invalid Square API Key")
return
}
color.Green("[!] Valid Square API Key\n\n")
color.Yellow("Merchant ID: %s", info.Permissions.MerchantID)
color.Yellow("Client ID: %s", info.Permissions.ClientID)
if info.Permissions.ExpiresAt == "" {
color.Green("Expires: Never\n\n")
} else {
color.Yellow("Expires: %s\n\n", info.Permissions.ExpiresAt)
}
printPermissions(info.Permissions.Scopes, cfg.ShowAll)
printTeamMembers(info.Team)
}
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
func printPermissions(scopes []string, showAll bool) {
isAccessToken := true
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"API Category", "Accessible Endpoints"})
for _, permissions_slice := range permissions_slice {
for category, permissions := range permissions_slice {
accessibleEndpoints := []string{}
for endpoint, requiredPermissions := range permissions {
hasAllPermissions := true
for _, permission := range requiredPermissions {
if !contains(scopes, permission) {
hasAllPermissions = false
isAccessToken = false
break
}
}
if hasAllPermissions {
accessibleEndpoints = append(accessibleEndpoints, endpoint)
}
}
if len(accessibleEndpoints) == 0 {
t.AppendRow([]interface{}{category, ""})
} else {
t.AppendRow([]interface{}{color.GreenString(category), color.GreenString(strings.Join(accessibleEndpoints, ", "))})
}
}
}
if isAccessToken {
color.Green("[i] Permissions: Full Access")
} else {
color.Yellow("[i] Permissions:")
}
if !isAccessToken || showAll {
t.SetColumnConfigs([]table.ColumnConfig{
{Number: 2, WidthMax: 100},
})
t.Render()
}
}
func printTeamMembers(team TeamJSON) {
if len(team.TeamMembers) == 0 {
color.Red("\n[x] No team members found")
return
}
color.Yellow("\n[i] Team Members (don't imply any permissions)")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"First Name", "Last Name", "Email", "Owner", "Created At"})
for _, member := range team.TeamMembers {
t.AppendRow([]interface{}{color.GreenString(member.FirstName), color.GreenString(member.LastName), color.GreenString(member.Email), color.GreenString(strconv.FormatBool(member.IsOwner)), color.GreenString(member.CreatedAt)})
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/square/square_test.go
================================================
package square
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
want []byte // JSON string
wantErr bool
}{
{
name: "valid Square key",
key: testSecrets.MustGetField("SQUARE_SECRET"),
want: expectedOutput,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal(tt.want, &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// // Bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// // Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/stripe/expected_output.json
================================================
{"AnalyzerType":19,"Bindings":[{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Customers:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Customer session:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Ephemeral keys:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Payment Method Domains:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"SetupIntents:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Sources:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Balance:Read","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Test clocks:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Files:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Funding Instructions:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"PaymentIntents:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"PaymentMethods:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Shipping Rates:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Tokens:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Charges:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Confirmation token:Read","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Confirmation token (client):Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Apple Pay Domains:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Disputes:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Events:Read","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Payouts:Write","Parent":null}},{"Resource":{"Name":"Core","FullyQualifiedName":"Core","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Products:Write","Parent":null}},{"Resource":{"Name":"Checkout","FullyQualifiedName":"Checkout","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Checkout Sessions:","Parent":null}},{"Resource":{"Name":"Billing","FullyQualifiedName":"Billing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Tax Rates:","Parent":null}},{"Resource":{"Name":"Billing","FullyQualifiedName":"Billing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Meter Events:","Parent":null}},{"Resource":{"Name":"Billing","FullyQualifiedName":"Billing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Coupons:","Parent":null}},{"Resource":{"Name":"Billing","FullyQualifiedName":"Billing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Promotion Codes:","Parent":null}},{"Resource":{"Name":"Billing","FullyQualifiedName":"Billing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Credit notes:","Parent":null}},{"Resource":{"Name":"Billing","FullyQualifiedName":"Billing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Subscriptions:","Parent":null}},{"Resource":{"Name":"Billing","FullyQualifiedName":"Billing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Quote:","Parent":null}},{"Resource":{"Name":"Billing","FullyQualifiedName":"Billing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Tax IDs:","Parent":null}},{"Resource":{"Name":"Billing","FullyQualifiedName":"Billing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Usage Records:","Parent":null}},{"Resource":{"Name":"Billing","FullyQualifiedName":"Billing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Meters:","Parent":null}},{"Resource":{"Name":"Billing","FullyQualifiedName":"Billing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Meter Event Adjustments:","Parent":null}},{"Resource":{"Name":"Billing","FullyQualifiedName":"Billing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Customer portal:","Parent":null}},{"Resource":{"Name":"Billing","FullyQualifiedName":"Billing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Invoices:","Parent":null}},{"Resource":{"Name":"Billing","FullyQualifiedName":"Billing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Prices:","Parent":null}},{"Resource":{"Name":"Connect","FullyQualifiedName":"Connect","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Transfers:Read","Parent":null}},{"Resource":{"Name":"Connect","FullyQualifiedName":"Connect","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Application Fees:Read","Parent":null}},{"Resource":{"Name":"Connect","FullyQualifiedName":"Connect","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Login Links:","Parent":null}},{"Resource":{"Name":"Connect","FullyQualifiedName":"Connect","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Account Links:","Parent":null}},{"Resource":{"Name":"Connect","FullyQualifiedName":"Connect","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Top-ups:Write","Parent":null}},{"Resource":{"Name":"Orders","FullyQualifiedName":"Orders","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Orders:","Parent":null}},{"Resource":{"Name":"Orders","FullyQualifiedName":"Orders","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"SKUs:","Parent":null}},{"Resource":{"Name":"Issuing","FullyQualifiedName":"Issuing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Tokens:","Parent":null}},{"Resource":{"Name":"Issuing","FullyQualifiedName":"Issuing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Transactions:","Parent":null}},{"Resource":{"Name":"Issuing","FullyQualifiedName":"Issuing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Authorizations:","Parent":null}},{"Resource":{"Name":"Issuing","FullyQualifiedName":"Issuing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Cardholders:","Parent":null}},{"Resource":{"Name":"Issuing","FullyQualifiedName":"Issuing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Cards:","Parent":null}},{"Resource":{"Name":"Issuing","FullyQualifiedName":"Issuing","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Disputes:","Parent":null}},{"Resource":{"Name":"Reporting","FullyQualifiedName":"Reporting","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Report Runs and Report Types:","Parent":null}},{"Resource":{"Name":"Identity","FullyQualifiedName":"Identity","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Verification Sessions and Reports:","Parent":null}},{"Resource":{"Name":"Webhook","FullyQualifiedName":"Webhook","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Webhook Endpoints:","Parent":null}},{"Resource":{"Name":"Payment Links","FullyQualifiedName":"Payment Links","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Payment Links:","Parent":null}},{"Resource":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Configurations:","Parent":null}},{"Resource":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Locations:","Parent":null}},{"Resource":{"Name":"Terminal","FullyQualifiedName":"Terminal","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Readers:","Parent":null}},{"Resource":{"Name":"Tax","FullyQualifiedName":"Tax","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Tax Calculations and Transactions:","Parent":null}},{"Resource":{"Name":"Tax","FullyQualifiedName":"Tax","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Tax Settings and Registrations:","Parent":null}},{"Resource":{"Name":"Radar","FullyQualifiedName":"Radar","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Reviews:","Parent":null}},{"Resource":{"Name":"Climate","FullyQualifiedName":"Climate","Type":"category","Metadata":null,"Parent":null},"Permission":{"Value":"Climate Orders:","Parent":null}}],"UnboundedResources":[{"Name":"Stripe CLI","FullyQualifiedName":"Stripe CLI","Type":"category","Metadata":null,"Parent":null}],"Metadata":{"key_env":"Test","key_type":"Restricted"}}
================================================
FILE: pkg/analyzer/analyzers/stripe/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package stripe
import "errors"
type Permission int
const (
Invalid Permission = iota
ConnectedAccountRead Permission = iota
AccountLinkWrite Permission = iota
ApplePayDomainRead Permission = iota
ApplePayDomainWrite Permission = iota
ApplicationFeeRead Permission = iota
ApplicationFeeWrite Permission = iota
BalanceRead Permission = iota
BalanceTransactionSourceRead Permission = iota
BillingClockRead Permission = iota
BillingClockWrite Permission = iota
ChargeRead Permission = iota
ChargeWrite Permission = iota
CheckoutSessionRead Permission = iota
CheckoutSessionWrite Permission = iota
TerminalConfigurationRead Permission = iota
TerminalConfigurationWrite Permission = iota
TerminalConnectionTokenWrite Permission = iota
CouponRead Permission = iota
CouponWrite Permission = iota
CreditNoteRead Permission = iota
CreditNoteWrite Permission = iota
CustomerPortalRead Permission = iota
CustomerPortalWrite Permission = iota
CustomerRead Permission = iota
CustomerWrite Permission = iota
DisputeRead Permission = iota
DisputeWrite Permission = iota
EditLinkWrite Permission = iota
ElementsWrite Permission = iota
EventRead Permission = iota
FileRead Permission = iota
FileWrite Permission = iota
InvoiceRead Permission = iota
InvoiceWrite Permission = iota
IssuingAuthorizationRead Permission = iota
IssuingAuthorizationWrite Permission = iota
IssuingCardRead Permission = iota
IssuingCardWrite Permission = iota
IssuingCardholderRead Permission = iota
IssuingCardholderWrite Permission = iota
IssuingDisputeRead Permission = iota
IssuingDisputeWrite Permission = iota
IssuingTransactionRead Permission = iota
IssuingTransactionWrite Permission = iota
TerminalLocationRead Permission = iota
TerminalLocationWrite Permission = iota
MandateRead Permission = iota
MandateWrite Permission = iota
OrderRead Permission = iota
OrderWrite Permission = iota
PaymentIntentRead Permission = iota
PaymentIntentWrite Permission = iota
PaymentLinksRead Permission = iota
PaymentLinksWrite Permission = iota
PaymentMethodRead Permission = iota
PaymentMethodWrite Permission = iota
PayoutRead Permission = iota
PayoutWrite Permission = iota
PlanRead Permission = iota
PlanWrite Permission = iota
ProductRead Permission = iota
ProductWrite Permission = iota
PromotionCodeRead Permission = iota
PromotionCodeWrite Permission = iota
QuoteRead Permission = iota
QuoteWrite Permission = iota
TerminalReaderRead Permission = iota
TerminalReaderWrite Permission = iota
ReportRunsAndReportTypesRead Permission = iota
ReviewRead Permission = iota
ReviewWrite Permission = iota
SecretWrite Permission = iota
SetupIntentRead Permission = iota
SetupIntentWrite Permission = iota
ShippingRateRead Permission = iota
ShippingRateWrite Permission = iota
SkuRead Permission = iota
SkuWrite Permission = iota
SourceRead Permission = iota
SourceWrite Permission = iota
SubscriptionRead Permission = iota
SubscriptionWrite Permission = iota
TaxRateRead Permission = iota
TaxRateWrite Permission = iota
TaxSettingsRead Permission = iota
TaxSettingsWrite Permission = iota
TaxCalculationsAndTransactionsRead Permission = iota
TaxCalculationsAndTransactionsWrite Permission = iota
TokenRead Permission = iota
TokenWrite Permission = iota
TopUpRead Permission = iota
TopUpWrite Permission = iota
TransferRead Permission = iota
TransferWrite Permission = iota
UsageRecordRead Permission = iota
UsageRecordWrite Permission = iota
UserEmailRead Permission = iota
WebhookRead Permission = iota
WebhookWrite Permission = iota
IssuingCardSensitiveRead Permission = iota
FundingInstructionRead Permission = iota
)
var (
PermissionStrings = map[Permission]string{
ConnectedAccountRead: "connected_account_read",
AccountLinkWrite: "account_link_write",
ApplePayDomainRead: "apple_pay_domain_read",
ApplePayDomainWrite: "apple_pay_domain_write",
ApplicationFeeRead: "application_fee_read",
ApplicationFeeWrite: "application_fee_write",
BalanceRead: "balance_read",
BalanceTransactionSourceRead: "balance_transaction_source_read",
BillingClockRead: "billing_clock_read",
BillingClockWrite: "billing_clock_write",
ChargeRead: "charge_read",
ChargeWrite: "charge_write",
CheckoutSessionRead: "checkout_session_read",
CheckoutSessionWrite: "checkout_session_write",
TerminalConfigurationRead: "terminal_configuration_read",
TerminalConfigurationWrite: "terminal_configuration_write",
TerminalConnectionTokenWrite: "terminal_connection_token_write",
CouponRead: "coupon_read",
CouponWrite: "coupon_write",
CreditNoteRead: "credit_note_read",
CreditNoteWrite: "credit_note_write",
CustomerPortalRead: "customer_portal_read",
CustomerPortalWrite: "customer_portal_write",
CustomerRead: "customer_read",
CustomerWrite: "customer_write",
DisputeRead: "dispute_read",
DisputeWrite: "dispute_write",
EditLinkWrite: "edit_link_write",
ElementsWrite: "elements_write",
EventRead: "event_read",
FileRead: "file_read",
FileWrite: "file_write",
InvoiceRead: "invoice_read",
InvoiceWrite: "invoice_write",
IssuingAuthorizationRead: "issuing_authorization_read",
IssuingAuthorizationWrite: "issuing_authorization_write",
IssuingCardRead: "issuing_card_read",
IssuingCardWrite: "issuing_card_write",
IssuingCardholderRead: "issuing_cardholder_read",
IssuingCardholderWrite: "issuing_cardholder_write",
IssuingDisputeRead: "issuing_dispute_read",
IssuingDisputeWrite: "issuing_dispute_write",
IssuingTransactionRead: "issuing_transaction_read",
IssuingTransactionWrite: "issuing_transaction_write",
TerminalLocationRead: "terminal_location_read",
TerminalLocationWrite: "terminal_location_write",
MandateRead: "mandate_read",
MandateWrite: "mandate_write",
OrderRead: "order_read",
OrderWrite: "order_write",
PaymentIntentRead: "payment_intent_read",
PaymentIntentWrite: "payment_intent_write",
PaymentLinksRead: "payment_links_read",
PaymentLinksWrite: "payment_links_write",
PaymentMethodRead: "payment_method_read",
PaymentMethodWrite: "payment_method_write",
PayoutRead: "payout_read",
PayoutWrite: "payout_write",
PlanRead: "plan_read",
PlanWrite: "plan_write",
ProductRead: "product_read",
ProductWrite: "product_write",
PromotionCodeRead: "promotion_code_read",
PromotionCodeWrite: "promotion_code_write",
QuoteRead: "quote_read",
QuoteWrite: "quote_write",
TerminalReaderRead: "terminal_reader_read",
TerminalReaderWrite: "terminal_reader_write",
ReportRunsAndReportTypesRead: "report_runs_and_report_types_read",
ReviewRead: "review_read",
ReviewWrite: "review_write",
SecretWrite: "secret_write",
SetupIntentRead: "setup_intent_read",
SetupIntentWrite: "setup_intent_write",
ShippingRateRead: "shipping_rate_read",
ShippingRateWrite: "shipping_rate_write",
SkuRead: "sku_read",
SkuWrite: "sku_write",
SourceRead: "source_read",
SourceWrite: "source_write",
SubscriptionRead: "subscription_read",
SubscriptionWrite: "subscription_write",
TaxRateRead: "tax_rate_read",
TaxRateWrite: "tax_rate_write",
TaxSettingsRead: "tax_settings_read",
TaxSettingsWrite: "tax_settings_write",
TaxCalculationsAndTransactionsRead: "tax_calculations_and_transactions_read",
TaxCalculationsAndTransactionsWrite: "tax_calculations_and_transactions_write",
TokenRead: "token_read",
TokenWrite: "token_write",
TopUpRead: "top_up_read",
TopUpWrite: "top_up_write",
TransferRead: "transfer_read",
TransferWrite: "transfer_write",
UsageRecordRead: "usage_record_read",
UsageRecordWrite: "usage_record_write",
UserEmailRead: "user_email_read",
WebhookRead: "webhook_read",
WebhookWrite: "webhook_write",
IssuingCardSensitiveRead: "issuing_card_sensitive_read",
FundingInstructionRead: "funding_instruction_read",
}
StringToPermission = map[string]Permission{
"connected_account_read": ConnectedAccountRead,
"account_link_write": AccountLinkWrite,
"apple_pay_domain_read": ApplePayDomainRead,
"apple_pay_domain_write": ApplePayDomainWrite,
"application_fee_read": ApplicationFeeRead,
"application_fee_write": ApplicationFeeWrite,
"balance_read": BalanceRead,
"balance_transaction_source_read": BalanceTransactionSourceRead,
"billing_clock_read": BillingClockRead,
"billing_clock_write": BillingClockWrite,
"charge_read": ChargeRead,
"charge_write": ChargeWrite,
"checkout_session_read": CheckoutSessionRead,
"checkout_session_write": CheckoutSessionWrite,
"terminal_configuration_read": TerminalConfigurationRead,
"terminal_configuration_write": TerminalConfigurationWrite,
"terminal_connection_token_write": TerminalConnectionTokenWrite,
"coupon_read": CouponRead,
"coupon_write": CouponWrite,
"credit_note_read": CreditNoteRead,
"credit_note_write": CreditNoteWrite,
"customer_portal_read": CustomerPortalRead,
"customer_portal_write": CustomerPortalWrite,
"customer_read": CustomerRead,
"customer_write": CustomerWrite,
"dispute_read": DisputeRead,
"dispute_write": DisputeWrite,
"edit_link_write": EditLinkWrite,
"elements_write": ElementsWrite,
"event_read": EventRead,
"file_read": FileRead,
"file_write": FileWrite,
"invoice_read": InvoiceRead,
"invoice_write": InvoiceWrite,
"issuing_authorization_read": IssuingAuthorizationRead,
"issuing_authorization_write": IssuingAuthorizationWrite,
"issuing_card_read": IssuingCardRead,
"issuing_card_write": IssuingCardWrite,
"issuing_cardholder_read": IssuingCardholderRead,
"issuing_cardholder_write": IssuingCardholderWrite,
"issuing_dispute_read": IssuingDisputeRead,
"issuing_dispute_write": IssuingDisputeWrite,
"issuing_transaction_read": IssuingTransactionRead,
"issuing_transaction_write": IssuingTransactionWrite,
"terminal_location_read": TerminalLocationRead,
"terminal_location_write": TerminalLocationWrite,
"mandate_read": MandateRead,
"mandate_write": MandateWrite,
"order_read": OrderRead,
"order_write": OrderWrite,
"payment_intent_read": PaymentIntentRead,
"payment_intent_write": PaymentIntentWrite,
"payment_links_read": PaymentLinksRead,
"payment_links_write": PaymentLinksWrite,
"payment_method_read": PaymentMethodRead,
"payment_method_write": PaymentMethodWrite,
"payout_read": PayoutRead,
"payout_write": PayoutWrite,
"plan_read": PlanRead,
"plan_write": PlanWrite,
"product_read": ProductRead,
"product_write": ProductWrite,
"promotion_code_read": PromotionCodeRead,
"promotion_code_write": PromotionCodeWrite,
"quote_read": QuoteRead,
"quote_write": QuoteWrite,
"terminal_reader_read": TerminalReaderRead,
"terminal_reader_write": TerminalReaderWrite,
"report_runs_and_report_types_read": ReportRunsAndReportTypesRead,
"review_read": ReviewRead,
"review_write": ReviewWrite,
"secret_write": SecretWrite,
"setup_intent_read": SetupIntentRead,
"setup_intent_write": SetupIntentWrite,
"shipping_rate_read": ShippingRateRead,
"shipping_rate_write": ShippingRateWrite,
"sku_read": SkuRead,
"sku_write": SkuWrite,
"source_read": SourceRead,
"source_write": SourceWrite,
"subscription_read": SubscriptionRead,
"subscription_write": SubscriptionWrite,
"tax_rate_read": TaxRateRead,
"tax_rate_write": TaxRateWrite,
"tax_settings_read": TaxSettingsRead,
"tax_settings_write": TaxSettingsWrite,
"tax_calculations_and_transactions_read": TaxCalculationsAndTransactionsRead,
"tax_calculations_and_transactions_write": TaxCalculationsAndTransactionsWrite,
"token_read": TokenRead,
"token_write": TokenWrite,
"top_up_read": TopUpRead,
"top_up_write": TopUpWrite,
"transfer_read": TransferRead,
"transfer_write": TransferWrite,
"usage_record_read": UsageRecordRead,
"usage_record_write": UsageRecordWrite,
"user_email_read": UserEmailRead,
"webhook_read": WebhookRead,
"webhook_write": WebhookWrite,
"issuing_card_sensitive_read": IssuingCardSensitiveRead,
"funding_instruction_read": FundingInstructionRead,
}
PermissionIDs = map[Permission]int{
ConnectedAccountRead: 1,
AccountLinkWrite: 2,
ApplePayDomainRead: 3,
ApplePayDomainWrite: 4,
ApplicationFeeRead: 5,
ApplicationFeeWrite: 6,
BalanceRead: 7,
BalanceTransactionSourceRead: 8,
BillingClockRead: 9,
BillingClockWrite: 10,
ChargeRead: 11,
ChargeWrite: 12,
CheckoutSessionRead: 13,
CheckoutSessionWrite: 14,
TerminalConfigurationRead: 15,
TerminalConfigurationWrite: 16,
TerminalConnectionTokenWrite: 17,
CouponRead: 18,
CouponWrite: 19,
CreditNoteRead: 20,
CreditNoteWrite: 21,
CustomerPortalRead: 22,
CustomerPortalWrite: 23,
CustomerRead: 24,
CustomerWrite: 25,
DisputeRead: 26,
DisputeWrite: 27,
EditLinkWrite: 28,
ElementsWrite: 29,
EventRead: 30,
FileRead: 31,
FileWrite: 32,
InvoiceRead: 33,
InvoiceWrite: 34,
IssuingAuthorizationRead: 35,
IssuingAuthorizationWrite: 36,
IssuingCardRead: 37,
IssuingCardWrite: 38,
IssuingCardholderRead: 39,
IssuingCardholderWrite: 40,
IssuingDisputeRead: 41,
IssuingDisputeWrite: 42,
IssuingTransactionRead: 43,
IssuingTransactionWrite: 44,
TerminalLocationRead: 45,
TerminalLocationWrite: 46,
MandateRead: 47,
MandateWrite: 48,
OrderRead: 49,
OrderWrite: 50,
PaymentIntentRead: 51,
PaymentIntentWrite: 52,
PaymentLinksRead: 53,
PaymentLinksWrite: 54,
PaymentMethodRead: 55,
PaymentMethodWrite: 56,
PayoutRead: 57,
PayoutWrite: 58,
PlanRead: 59,
PlanWrite: 60,
ProductRead: 61,
ProductWrite: 62,
PromotionCodeRead: 63,
PromotionCodeWrite: 64,
QuoteRead: 65,
QuoteWrite: 66,
TerminalReaderRead: 67,
TerminalReaderWrite: 68,
ReportRunsAndReportTypesRead: 69,
ReviewRead: 70,
ReviewWrite: 71,
SecretWrite: 72,
SetupIntentRead: 73,
SetupIntentWrite: 74,
ShippingRateRead: 75,
ShippingRateWrite: 76,
SkuRead: 77,
SkuWrite: 78,
SourceRead: 79,
SourceWrite: 80,
SubscriptionRead: 81,
SubscriptionWrite: 82,
TaxRateRead: 83,
TaxRateWrite: 84,
TaxSettingsRead: 85,
TaxSettingsWrite: 86,
TaxCalculationsAndTransactionsRead: 87,
TaxCalculationsAndTransactionsWrite: 88,
TokenRead: 89,
TokenWrite: 90,
TopUpRead: 91,
TopUpWrite: 92,
TransferRead: 93,
TransferWrite: 94,
UsageRecordRead: 95,
UsageRecordWrite: 96,
UserEmailRead: 97,
WebhookRead: 98,
WebhookWrite: 99,
IssuingCardSensitiveRead: 100,
FundingInstructionRead: 101,
}
IdToPermission = map[int]Permission{
1: ConnectedAccountRead,
2: AccountLinkWrite,
3: ApplePayDomainRead,
4: ApplePayDomainWrite,
5: ApplicationFeeRead,
6: ApplicationFeeWrite,
7: BalanceRead,
8: BalanceTransactionSourceRead,
9: BillingClockRead,
10: BillingClockWrite,
11: ChargeRead,
12: ChargeWrite,
13: CheckoutSessionRead,
14: CheckoutSessionWrite,
15: TerminalConfigurationRead,
16: TerminalConfigurationWrite,
17: TerminalConnectionTokenWrite,
18: CouponRead,
19: CouponWrite,
20: CreditNoteRead,
21: CreditNoteWrite,
22: CustomerPortalRead,
23: CustomerPortalWrite,
24: CustomerRead,
25: CustomerWrite,
26: DisputeRead,
27: DisputeWrite,
28: EditLinkWrite,
29: ElementsWrite,
30: EventRead,
31: FileRead,
32: FileWrite,
33: InvoiceRead,
34: InvoiceWrite,
35: IssuingAuthorizationRead,
36: IssuingAuthorizationWrite,
37: IssuingCardRead,
38: IssuingCardWrite,
39: IssuingCardholderRead,
40: IssuingCardholderWrite,
41: IssuingDisputeRead,
42: IssuingDisputeWrite,
43: IssuingTransactionRead,
44: IssuingTransactionWrite,
45: TerminalLocationRead,
46: TerminalLocationWrite,
47: MandateRead,
48: MandateWrite,
49: OrderRead,
50: OrderWrite,
51: PaymentIntentRead,
52: PaymentIntentWrite,
53: PaymentLinksRead,
54: PaymentLinksWrite,
55: PaymentMethodRead,
56: PaymentMethodWrite,
57: PayoutRead,
58: PayoutWrite,
59: PlanRead,
60: PlanWrite,
61: ProductRead,
62: ProductWrite,
63: PromotionCodeRead,
64: PromotionCodeWrite,
65: QuoteRead,
66: QuoteWrite,
67: TerminalReaderRead,
68: TerminalReaderWrite,
69: ReportRunsAndReportTypesRead,
70: ReviewRead,
71: ReviewWrite,
72: SecretWrite,
73: SetupIntentRead,
74: SetupIntentWrite,
75: ShippingRateRead,
76: ShippingRateWrite,
77: SkuRead,
78: SkuWrite,
79: SourceRead,
80: SourceWrite,
81: SubscriptionRead,
82: SubscriptionWrite,
83: TaxRateRead,
84: TaxRateWrite,
85: TaxSettingsRead,
86: TaxSettingsWrite,
87: TaxCalculationsAndTransactionsRead,
88: TaxCalculationsAndTransactionsWrite,
89: TokenRead,
90: TokenWrite,
91: TopUpRead,
92: TopUpWrite,
93: TransferRead,
94: TransferWrite,
95: UsageRecordRead,
96: UsageRecordWrite,
97: UserEmailRead,
98: WebhookRead,
99: WebhookWrite,
100: IssuingCardSensitiveRead,
101: FundingInstructionRead,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/stripe/permissions.yaml
================================================
permissions:
- connected_account_read
- account_link_write
- apple_pay_domain_read
- apple_pay_domain_write
- application_fee_read
- application_fee_write
- balance_read
- balance_transaction_source_read
- billing_clock_read
- billing_clock_write
- charge_read
- charge_write
- checkout_session_read
- checkout_session_write
- terminal_configuration_read
- terminal_configuration_write
- terminal_connection_token_write
- coupon_read
- coupon_write
- credit_note_read
- credit_note_write
- customer_portal_read
- customer_portal_write
- customer_read
- customer_write
- dispute_read
- dispute_write
- edit_link_write
- elements_write
- event_read
- file_read
- file_write
- invoice_read
- invoice_write
- issuing_authorization_read
- issuing_authorization_write
- issuing_card_read
- issuing_card_write
- issuing_cardholder_read
- issuing_cardholder_write
- issuing_dispute_read
- issuing_dispute_write
- issuing_transaction_read
- issuing_transaction_write
- terminal_location_read
- terminal_location_write
- mandate_read
- mandate_write
- order_read
- order_write
- payment_intent_read
- payment_intent_write
- payment_links_read
- payment_links_write
- payment_method_read
- payment_method_write
- payout_read
- payout_write
- plan_read
- plan_write
- product_read
- product_write
- promotion_code_read
- promotion_code_write
- quote_read
- quote_write
- terminal_reader_read
- terminal_reader_write
- report_runs_and_report_types_read
- review_read
- review_write
- secret_write
- setup_intent_read
- setup_intent_write
- shipping_rate_read
- shipping_rate_write
- sku_read
- sku_write
- source_read
- source_write
- subscription_read
- subscription_write
- tax_rate_read
- tax_rate_write
- tax_settings_read
- tax_settings_write
- tax_calculations_and_transactions_read
- tax_calculations_and_transactions_write
- token_read
- token_write
- top_up_read
- top_up_write
- transfer_read
- transfer_write
- usage_record_read
- usage_record_write
- user_email_read
- webhook_read
- webhook_write
- issuing_card_sensitive_read
- funding_instruction_read
================================================
FILE: pkg/analyzer/analyzers/stripe/restricted.yaml
================================================
categories:
Core:
Apple Pay Domains:
Read:
Scope: rak_apple_pay_domain_read
Endpoint: https://api.stripe.com/v1/apple_pay/domains
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://any-api.com/stripe_com/stripe_com/docs/_v1_apple_pay_domains/GetApplePayDomains
Note: ''
Write:
Scope: rak_apple_pay_domain_write
Endpoint: https://api.stripe.com/v1/apple_pay/domains
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: ''
Note: ''
Balance:
Read:
Scope: rak_balance_read
Endpoint: https://api.stripe.com/v1/balance
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/balance
Note: ''
Balance transaction sources:
Read:
Scope: ''
Endpoint: ''
Method: ''
Payload: ''
Valid: []
Invalid: []
Docs: Can't find a relevant endpoint
Note: 'I think we just build this one based off of all the others? Note that
this permission also implies the following permissions: Application Fees
(Read), Balance (Read), Financing Transactions (Read), Payouts (Read), Transfers
(Read), and Balance Transfers (Read)'
Balance Transfer:
Read:
Scope: ''
Endpoint: ''
Method: ''
Payload: ''
Valid: []
Invalid: []
Docs: Can't find a relevant endpoint
Note: Not sure this exists anymore
Write:
Scope: ''
Endpoint: ''
Method: ''
Payload: ''
Valid: []
Invalid: []
Docs: Can't find a relevant endpoint
Note: Not sure this exists anymore
Test clocks:
Read:
Scope: rak_billing_clock_read
Endpoint: https://api.stripe.com/v1/test_helpers/test_clocks
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/test_clocks/list
Note: ''
Write:
Scope: rak_billing_clock_write
Endpoint: https://api.stripe.com/v1/test_helpers/test_clocks
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/test_clocks/create
Note: ''
Charges:
Read:
Scope: rak_charge_read
Endpoint: https://api.stripe.com/v1/charges
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/charges/list
Note: ''
Write:
Scope: rak_charge_write
Endpoint: https://api.stripe.com/v1/charges
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/charges/update
Note: ''
Confirmation token:
Read:
Scope: rak_confirmation_token_read
Endpoint: https://api.stripe.com/v1/confirmation_tokens/nowaythiscanexist
Method: GET
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/confirmation_tokens/retrieve
Note: ''
Confirmation token (client):
Read:
Scope: ''
Endpoint: ''
Method: ''
Payload: ''
Valid: []
Invalid: []
Docs: Can't find a relevant endpoint
Note: Not sure this exists anymore
Write:
Scope: rak_confirmation_token_client_write
Endpoint: https://api.stripe.com/v1/test_helpers/confirmation_tokens
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/confirmation_tokens/test_create
Note: ''
Customers:
Read:
Scope: rak_customer_read
Endpoint: https://api.stripe.com/v1/customers
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/customers/list
Note: ''
Write:
Scope: rak_customer_write
Endpoint: https://api.stripe.com/v1/customers/nowaythiscanexist
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/customers/update
Note: Couldn't use "Create Customer", b/c default with no payload creates
a customer.
Customer session:
Read:
Scope: ''
Endpoint: ''
Method: ''
Payload: ''
Valid: []
Invalid: []
Docs: Can't find a relevant endpoint
Note: Not sure this exists anymore
Write:
Scope: rak_customer_session_write
Endpoint: https://api.stripe.com/v1/customer_sessions
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/customer_sessions/create
Note: ''
Disputes:
Read:
Scope: rak_dispute_read
Endpoint: https://api.stripe.com/v1/disputes
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/disputes/list
Note: ''
Write:
Scope: rak_dispute_write
Endpoint: https://api.stripe.com/v1/disputes/nowaycanthisexist
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/disputes/update
Note: ''
Events:
Read:
Scope: rak_event_read
Endpoint: https://api.stripe.com/v1/events
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/events/list
Note: ''
Ephemeral keys:
Read:
Scope: ''
Endpoint: ''
Method: ''
Payload: ''
Valid: []
Invalid: []
Docs: Can't find a relevant endpoint
Note: ''
Write:
Scope: rak_ephemeral_key_write
Endpoint: https://api.stripe.com/v1/ephemeral_keys
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://any-api.com/stripe_com/stripe_com/docs/_v1_ephemeral_keys_key_
Note: ''
Files:
Read:
Scope: rak_file_read
Endpoint: https://api.stripe.com/v1/files
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: ''
Note: ''
Write:
Scope: ''
Endpoint: https://files.stripe.com/v1/files
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: ''
Note: On 403, it mistakenly says "rak_dispute_write" missing
Funding Instructions:
Read:
Scope: ''
Endpoint: https://api.stripe.com/v1/issuing/funding_instructions
Method: GET
Payload: ''
Valid:
- 200
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/issuing/funding_instructions/list
Note: On 403, it mistakently says "rak_topup_read"
Write:
Scope: ''
Endpoint: https://api.stripe.com/v1/issuing/funding_instructions
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/issuing/funding_instructions/create
Note: Same as read but says "write"
PaymentIntents:
Read:
Scope: rak_payment_intent_read
Endpoint: https://api.stripe.com/v1/payment_intents
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/payment_intents/list
Note: ''
Write:
Scope: rak_payment_intent_write
Endpoint: https://api.stripe.com/v1/payment_intents
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/payment_intents/create
Note: ''
PaymentMethods:
Read:
Scope: rak_payment_method_read
Endpoint: https://api.stripe.com/v1/payment_methods
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://any-api.com/stripe_com/stripe_com/docs/_v1_payment_methods/GetPaymentMethods
Note: ''
Write:
Scope: rak_payment_method_write
Endpoint: https://api.stripe.com/v1/payment_methods/nowaycanthisexist
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://any-api.com/stripe_com/stripe_com/docs/_v1_payment_methods_payment_method_/PostPaymentMethodsPaymentMethod
Note: ''
Payment Method Domains:
Read:
Scope: ''
Endpoint: https://api.stripe.com/v1/payment_method_domains
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/payment_method_domains/list
Note: ''
Write:
Scope: ''
Endpoint: https://api.stripe.com/v1/payment_method_domains
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/payment_method_domains/create
Note: ''
Payouts:
Read:
Scope: rak_payout_read
Endpoint: https://api.stripe.com/v1/payouts
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/payouts/list
Note: ''
Write:
Scope: rak_payout_write
Endpoint: https://api.stripe.com/v1/payouts
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/payouts/create
Note: ''
Products:
Read:
Scope: rak_product_read
Endpoint: https://api.stripe.com/v1/products
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/products/list
Note: ''
Write:
Scope: rak_product_write
Endpoint: https://api.stripe.com/v1/products
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/products/create
Note: ''
Shipping Rates:
Read:
Scope: rak_shipping_rate_read
Endpoint: https://api.stripe.com/v1/shipping_rates
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/shipping_rates/list
Note: ''
Write:
Scope: rak_shipping_rate_write
Endpoint: https://api.stripe.com/v1/shipping_rates
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/shipping_rates/create
Note: ''
SetupIntents:
Read:
Scope: rak_setup_intent_read
Endpoint: https://api.stripe.com/v1/setup_intents
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/setup_intents/list
Note: ''
Write:
Scope: rak_setup_intent_write
Endpoint: https://api.stripe.com/v1/setup_intents/nowaycanthisexist
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/setup_intents/create
Note: ''
Sources:
Read:
Scope: rak_source_read
Endpoint: https://api.stripe.com/v1/sources/nowaycanthisexist
Method: GET
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/sources/retrieve
Note: ''
Write:
Scope: rak_source_write
Endpoint: https://api.stripe.com/v1/sources/nowaycanthisexist
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/sources/update
Note: ''
Tokens:
Read:
Scope: rak_token_read
Endpoint: https://api.stripe.com/v1/tokens/nowaycanthisexist
Method: GET
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/tokens/retrieve
Note: ''
Write:
Scope: rak_token_write
Endpoint: https://api.stripe.com/v1/tokens
Method: POST
Payload: '"card[number]"=4242424242424242'
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/tokens/create_card
Note: ''
Checkout:
Checkout Sessions:
Read:
Scope: rak_checkout_session_read
Endpoint: https://api.stripe.com/v1/checkout/sessions
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/checkout/sessions/list
Note: ''
Write:
Scope: rak_checkout_session_write
Endpoint: https://api.stripe.com/v1/checkout/sessions
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/checkout/sessions/create
Note: ''
Billing:
Coupons:
Read:
Scope: rak_coupon_read
Endpoint: https://api.stripe.com/v1/coupons
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/coupons/list
Note: ''
Write:
Scope: rak_coupon_write
Endpoint: https://api.stripe.com/v1/coupons
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/coupons/create
Note: ''
Promotion Codes:
Read:
Scope: rak_promotion_code_read
Endpoint: https://api.stripe.com/v1/promotion_codes
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/promotion_codes/list
Note: ''
Write:
Scope: rak_promotion_code_write
Endpoint: https://api.stripe.com/v1/promotion_codes
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/promotion_codes/create
Note: ''
Credit notes:
Read:
Scope: rak_credit_note_read
Endpoint: https://api.stripe.com/v1/credit_notes
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/credit_notes/list
Note: ''
Write:
Scope: rak_credit_note_write
Endpoint: https://api.stripe.com/v1/credit_notes/nowaythiscanexsit
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/credit_notes/update
Note: ''
Customer portal:
Read:
Scope: ''
Endpoint: ''
Method: ''
Payload: ''
Valid: []
Invalid: []
Docs: Can't find a relevant endpoint
Note: ''
Write:
Scope: rak_customer_portal_write
Endpoint: https://api.stripe.com/v1/billing_portal/sessions
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/customer_portal/sessions/create
Note: ''
Invoices:
Read:
Scope: ''
Endpoint: https://api.stripe.com/v1/invoices
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/invoices/list
Note: Wrong scope in error message.
Write:
Scope: rak_invoice_write
Endpoint: https://api.stripe.com/v1/invoices
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/invoices/create
Note: ''
Prices:
Read:
Scope: rak_plan_read
Endpoint: https://api.stripe.com/v1/prices
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/prices/list
Note: ''
Write:
Scope: rak_plan_write
Endpoint: https://api.stripe.com/v1/prices
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/prices/create
Note: ''
Subscriptions:
Read:
Scope: rak_subscription_read
Endpoint: https://api.stripe.com/v1/subscriptions
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/subscriptions/list
Note: ''
Write:
Scope: rak_subscription_write
Endpoint: https://api.stripe.com/v1/subscriptions
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/subscriptions/create
Note: ''
Quote:
Read:
Scope: rak_quote_read
Endpoint: https://api.stripe.com/v1/quotes
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/quotes/list
Note: ''
Write:
Scope: rak_quote_write
Endpoint: https://api.stripe.com/v1/quotes/nowaythiscanexist
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/quotes/update
Note: ''
Tax IDs:
Read:
Scope: rak_tax_id_read
Endpoint: https://api.stripe.com/v1/tax_ids
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/tax_ids/list
Note: ''
Write:
Scope: rak_tax_id_write
Endpoint: https://api.stripe.com/v1/tax_ids
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/tax_ids/create
Note: ''
Tax Rates:
Read:
Scope: rak_tax_rate_read
Endpoint: https://api.stripe.com/v1/tax_rates
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/tax_rates/list
Note: ''
Write:
Scope: rak_tax_rate_write
Endpoint: https://api.stripe.com/v1/tax_rates
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/tax_rates/create
Note: ''
Usage Records:
Read:
Scope: rak_usage_record_read
Endpoint: https://api.stripe.com/v1/subscription_items/nowaythiscanexist/usage_record_summaries
Method: GET
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/usage_records/subscription_item_summary_list
Note: ''
Write:
Scope: rak_usage_record_write
Endpoint: https://api.stripe.com/v1/subscription_items/nowaythiscanexist/usage_records
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/usage_records/create
Note: ''
Meters:
Read:
Scope: rak_billing_meter_read
Endpoint: https://api.stripe.com/v1/billing/meters
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/billing/meter/list
Note: ''
Write:
Scope: rak_billing_meter_write
Endpoint: https://api.stripe.com/v1/billing/meters
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/billing/meter/create
Note: ''
Meter Events:
Read:
Scope: rak_billing_meter_event_read
Endpoint: https://api.stripe.com/v1/billing/meters/nowaythiscanexist/event_summaries
Method: GET
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/billing/meter-event_summary/list
Note: ''
Write:
Scope: rak_billing_meter_event_write
Endpoint: https://api.stripe.com/v1/billing/meter_events
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/billing/meter-event/create
Note: ''
Meter Event Adjustments:
Write:
Scope: rak_billing_meter_event_adjustment_write
Endpoint: https://api.stripe.com/v1/billing/meter_event_adjustments
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/billing/meter-event_adjustment/create
Note: ''
Connect:
Application Fees:
Read:
Scope: rak_application_fee_read
Endpoint: https://api.stripe.com/v1/application_fees
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/application_fees/list
Note: ''
Write:
Scope: rak_application_fee_write
Endpoint: https://api.stripe.com/v1/application_fees/nowaythiscanexist/refunds
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/fee_refunds/create
Note: ''
Login Links:
Write:
Scope: rak_edit_link_write
Endpoint: https://api.stripe.com/v1/account/login_links
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://any-api.com/stripe_com/stripe_com/docs/_v1_account_login_links/PostAccountLoginLinks
Note: ''
Account Links:
Write:
Scope: rak_account_link_write
Endpoint: https://api.stripe.com/v1/account_links
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/account_links
Note: ''
Top-ups:
Read:
Scope: rak_topup_read
Endpoint: https://api.stripe.com/v1/topups
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/topups/list
Note: ''
Write:
Scope: rak_topup_write
Endpoint: https://api.stripe.com/v1/topups
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/topups/create
Note: ''
Transfers:
Read:
Scope: rak_transfer_read
Endpoint: https://api.stripe.com/v1/transfers
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/transfers/list
Note: ''
Write:
Scope: rak_transfer_write
Endpoint: https://api.stripe.com/v1/transfers
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/transfers/create
Note: ''
Orders:
Orders:
Read:
Scope: rak_order_read
Endpoint: https://api.stripe.com/v1/orders
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://any-api.com/stripe_com/stripe_com/docs/_v1_orders/GetOrders
Note: ''
Write:
Scope: rak_order_write
Endpoint: https://api.stripe.com/v1/orders
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://any-api.com/stripe_com/stripe_com/docs/_v1_orders/PostOrders
Note: ''
SKUs:
Read:
Scope: ''
Endpoint: ''
Method: ''
Payload: ''
Valid: []
Invalid: []
Docs: Can't find a relevant endpoint
Note: Seems like any key has 200 over these.
Write:
Scope: rak_sku_write
Endpoint: https://api.stripe.com/v1/skus
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://any-api.com/stripe_com/stripe_com/docs/_v1_skus/PostSkus
Note: ''
Issuing:
Authorizations:
Read:
Scope: rak_issuing_authorization_read
Endpoint: https://api.stripe.com/v1/issuing/authorizations/nowaythiscanexist
Method: GET
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/issuing/authorizations/retrieve
Note: ''
Write:
Scope: rak_issuing_authorization_write
Endpoint: https://api.stripe.com/v1/issuing/authorizations/nowaythiscanexist
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/issuing/authorizations/update
Note: ''
Cardholders:
Read:
Scope: rak_issuing_cardholder_read
Endpoint: https://api.stripe.com/v1/issuing/cardholders/nowaythiscanexist
Method: GET
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/issuing/cardholders/retrieve
Note: ''
Write:
Scope: rak_issuing_cardholder_write
Endpoint: https://api.stripe.com/v1/issuing/cardholders
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/issuing/cardholders/create
Note: ''
Cards:
Read:
Scope: rak_issuing_card_read
Endpoint: https://api.stripe.com/v1/issuing/cards/nowaythiscanexist
Method: GET
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/issuing/cards/retrieve
Note: ''
Write:
Scope: rak_issuing_card_write
Endpoint: https://api.stripe.com/v1/issuing/cards
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/issuing/cards/create
Note: ''
Disputes:
Read:
Scope: rak_issuing_dispute_read
Endpoint: https://api.stripe.com/v1/issuing/disputes/nowaythiscanexist
Method: GET
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/issuing/disputes/retrieve
Note: ''
Write:
Scope: rak_issuing_dispute_write
Endpoint: https://api.stripe.com/v1/issuing/disputes/nowaythiscanexist
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/issuing/disputes/update
Note: ''
Tokens:
Read:
Scope: rak_issuing_network_token_read
Endpoint: https://api.stripe.com/v1/issuing/tokens/nowaythiscanexist
Method: GET
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/issuing/tokens/retrieve
Note: ''
Write:
Scope: rak_issuing_network_token_write
Endpoint: https://api.stripe.com/v1/issuing/tokens/nowaythiscanexist
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/issuing/tokens/update
Note: ''
Token Network Data:
Read:
Scope: ''
Endpoint: ''
Method: ''
Payload: ''
Valid: []
Invalid: []
Docs: Can't find a relevant endpoint
Note: ''
Transactions:
Read:
Scope: rak_issuing_transaction_read
Endpoint: https://api.stripe.com/v1/issuing/transactions/nowaythiscanexist
Method: GET
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/issuing/transactions/retrieve
Note: ''
Write:
Scope: rak_issuing_transaction_write
Endpoint: https://api.stripe.com/v1/issuing/transactions/nowaythiscanexist
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/issuing/transactions/update
Note: ''
Reporting:
Report Runs and Report Types:
Read:
Scope: rak_financial_statement_read
Endpoint: https://api.stripe.com/v1/reporting/report_runs
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/reporting/report_run/list
Note: ''
Identity:
Verification Sessions and Reports:
Read:
Scope: rak_identity_product_read
Endpoint: https://api.stripe.com/v1/identity/verification_sessions
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/identity/verification_sessions/list
Note: ''
Write:
Scope: rak_identity_product_write
Endpoint: https://api.stripe.com/v1/identity/verification_sessions
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/identity/verification_sessions/create
Note: ''
Access recent detailed verification results:
Read:
Scope: ''
Endpoint: ''
Method: ''
Payload: ''
Valid: []
Invalid: []
Docs: Can't find a relevant endpoint
Note: Skip for now b/c requires account with data
Access all detailed verification results:
Read:
Scope: ''
Endpoint: ''
Method: ''
Payload: ''
Valid: []
Invalid: []
Docs: Can't find a relevant endpoint
Note: Skip for now b/c requires account with data + this one requires IP allowlisting
Webhook:
Webhook Endpoints:
Read:
Scope: rak_webhook_read
Endpoint: https://api.stripe.com/v1/webhook_endpoints
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/webhook_endpoints/list
Note: ''
Write:
Scope: rak_webhook_write
Endpoint: https://api.stripe.com/v1/webhook_endpoints
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/webhook_endpoints/create
Note: ''
Stripe CLI:
Debugging tools:
Write:
Scope: ''
Endpoint: ''
Method: ''
Payload: ''
Valid: []
Invalid: []
Docs: Can't find a relevant endpoint
Note: ''
Payment Links:
Payment Links:
Read:
Scope: rak_payment_links_read
Endpoint: https://api.stripe.com/v1/payment_links
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/payment_links/payment_links/list
Note: ''
Write:
Scope: rak_payment_links_write
Endpoint: https://api.stripe.com/v1/payment_links
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/payment_links/payment_links/create
Note: ''
Terminal:
Configurations:
Read:
Scope: rak_terminal_configuration_read
Endpoint: https://api.stripe.com/v1/terminal/configurations
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/terminal/configuration/list
Note: ''
Write:
Scope: rak_terminal_configuration_write
Endpoint: https://api.stripe.com/v1/terminal/configurations/nowaythiscanexist
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/terminal/configuration/update
Note: ''
Locations:
Read:
Scope: rak_terminal_location_read
Endpoint: https://api.stripe.com/v1/terminal/locations
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/terminal/locations/list
Note: ''
Write:
Scope: rak_terminal_location_write
Endpoint: https://api.stripe.com/v1/terminal/locations
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/terminal/locations/create
Note: ''
Readers:
Read:
Scope: rak_terminal_reader_read
Endpoint: https://api.stripe.com/v1/terminal/readers
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/terminal/readers/list
Note: ''
Write:
Scope: rak_terminal_reader_write
Endpoint: https://api.stripe.com/v1/terminal/readers
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/terminal/readers/create
Note: ''
Connection Tokens:
Write:
Scope: rak_terminal_connection_token_write
Endpoint: ''
Method: POST
Payload: ''
Valid: []
Invalid: []
Docs: ''
Note: Skip b/c requires a state change.
Tax:
Tax Calculations and Transactions:
Read:
Scope: rak_tax_transaction_read
Endpoint: https://api.stripe.com/v1/tax/calculations/nowaycanthisexist/line_items
Method: GET
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/tax/calculations/line_items
Note: ''
Write:
Scope: rak_tax_transaction_write
Endpoint: https://api.stripe.com/v1/tax/calculations
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/tax/calculations/create
Note: ''
Tax Settings and Registrations:
Read:
Scope: rak_tax_settings_read
Endpoint: https://api.stripe.com/v1/tax/settings
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/tax/settings/retrieve
Note: ''
Write:
Scope: rak_tax_settings_write
Endpoint: https://api.stripe.com/v1/tax/registrations/nowaycanthisexist
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/tax/registrations/update
Note: ''
Radar:
Reviews:
Read:
Scope: rak_review_read
Endpoint: https://api.stripe.com/v1/reviews
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/radar/reviews/list
Note: ''
Write:
Scope: rak_review_write
Endpoint: https://api.stripe.com/v1/reviews/nowaycanthisexist/approve
Method: POST
Payload: ''
Valid:
- 404
Invalid:
- 403
Docs: https://docs.stripe.com/api/radar/reviews/approve
Note: ''
Climate:
Climate Orders:
Read:
Scope: rak_climate_order_read
Endpoint: https://api.stripe.com/v1/climate/orders
Method: GET
Payload: ''
Valid:
- 200
Invalid:
- 403
Docs: https://docs.stripe.com/api/climate/order/list
Note: ''
Write:
Scope: rak_climate_order_write
Endpoint: https://api.stripe.com/v1/climate/orders
Method: POST
Payload: ''
Valid:
- 400
Invalid:
- 403
Docs: https://docs.stripe.com/api/climate/order/create
Note: ''
================================================
FILE: pkg/analyzer/analyzers/stripe/stripe.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go stripe
package stripe
import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"sort"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"gopkg.in/yaml.v2"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeStripe }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, errors.New("key not found in credentialInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := &analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeStripe,
Metadata: map[string]any{
"key_type": info.KeyType,
"key_env": info.KeyEnv,
},
}
// create list of bindings using permissions, with category being the parent and unbounded resource
result.Bindings = []analyzers.Binding{}
result.UnboundedResources = []analyzers.Resource{}
for _, permissionCategory := range info.Permissions {
parentResource := &analyzers.Resource{
Name: permissionCategory.Name,
FullyQualifiedName: permissionCategory.Name,
Type: "category",
Metadata: nil,
Parent: nil,
}
if len(permissionCategory.Permissions) == 0 {
result.UnboundedResources = append(result.UnboundedResources, *parentResource)
} else {
for _, permission := range permissionCategory.Permissions {
if _, ok := StringToPermission[*permission.Value]; !ok { // skip unknown scopes/permission
continue
}
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: *parentResource,
Permission: analyzers.Permission{
Value: fmt.Sprintf("%s:%s", permission.Name, *permission.Value),
},
})
}
}
}
return result
}
const (
SECRET_PREFIX = "sk_"
PUBLISHABLE_PREFIX = "pk_"
RESTRICTED_PREFIX = "rk_"
LIVE_PREFIX = "live_"
TEST_PREFIX = "test_"
SECRET = "Secret"
PUBLISHABLE = "Publishable"
RESTRICTED = "Restricted"
LIVE = "Live"
TEST = "Test"
)
//go:embed restricted.yaml
var restrictedConfig []byte
type PermissionStruct struct {
Name string
Value *string
}
type PermissionsCategory struct {
Name string
Permissions []PermissionStruct
}
type HttpStatusTest struct {
Endpoint string `yaml:"Endpoint"`
Method string `yaml:"Method"`
Payload interface{} `yaml:"Payload"`
ValidStatuses []int `yaml:"Valid"`
InvalidStatuses []int `yaml:"Invalid"`
}
type Category map[string]map[string]HttpStatusTest
type Config struct {
Categories map[string]Category `yaml:"categories"`
}
type SecretInfo struct {
KeyType string
KeyEnv string
Valid bool
Permissions []PermissionsCategory
}
func (h *HttpStatusTest) RunTest(cfg *config.Config, headers map[string]string) (bool, error) {
// If body data, marshal to JSON
var data io.Reader
if h.Payload != nil {
jsonData, err := json.Marshal(h.Payload)
if err != nil {
return false, err
}
data = bytes.NewBuffer(jsonData)
}
// Create new HTTP request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest(h.Method, h.Endpoint, data)
if err != nil {
return false, err
}
// Add custom headers if provided
for key, value := range headers {
req.Header.Set(key, value)
}
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// Check response status code
switch {
case StatusContains(resp.StatusCode, h.ValidStatuses):
return true, nil
case StatusContains(resp.StatusCode, h.InvalidStatuses):
return false, nil
default:
fmt.Println(h)
fmt.Println(resp.Body)
fmt.Println(resp.StatusCode)
return false, errors.New("error checking response status code")
}
}
func StatusContains(status int, vals []int) bool {
for _, v := range vals {
if status == v {
return true
}
}
return false
}
func checkKeyType(key string) (string, error) {
if strings.HasPrefix(key, SECRET_PREFIX) {
return SECRET, nil
} else if strings.HasPrefix(key, PUBLISHABLE_PREFIX) {
return PUBLISHABLE, nil
} else if strings.HasPrefix(key, RESTRICTED_PREFIX) {
return RESTRICTED, nil
}
return "", errors.New("Invalid Stripe key format")
}
func checkKeyEnv(key string) (string, error) {
//remove first 3 characters
key = key[3:]
if strings.HasPrefix(key, LIVE_PREFIX) {
return LIVE, nil
}
if strings.HasPrefix(key, TEST_PREFIX) {
return TEST, nil
}
return "", errors.New("invalid Stripe key format")
}
func checkValidity(cfg *config.Config, key string) (bool, error) {
// Create a new request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://api.stripe.com/v1/charges", nil)
if err != nil {
return false, err
}
// Add Authorization header
req.Header.Add("Authorization", "Bearer "+key)
// Send the request
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// Check the response. Valid is 200 (secret/restricted) or 403 (restricted)
if resp.StatusCode == 200 || resp.StatusCode == 403 {
return true, nil
}
return false, nil
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
// Check if secret, publishable, or restricted key
var keyType, keyEnv string
keyType, err := checkKeyType(key)
if err != nil {
return nil, err
}
// Check if live or test key
keyEnv, err = checkKeyEnv(key)
if err != nil {
return nil, err
}
// Check if key is valid
valid, err := checkValidity(cfg, key)
if err != nil {
return nil, err
}
permissions, err := getRestrictedPermissions(cfg, key)
if err != nil {
return nil, err
}
// Additional details
// get total customers
// get total charges
return &SecretInfo{
KeyType: keyType,
KeyEnv: keyEnv,
Valid: valid,
Permissions: permissions,
}, nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
if info.KeyType == PUBLISHABLE {
color.Red("[x] This is a publishable Stripe key. It is not considered secret.")
return
}
if !info.Valid {
color.Red("[x] Invalid Stripe API Key\n")
return
}
color.Green("[!] Valid Stripe API Key\n\n")
if info.KeyType == SECRET {
color.Green("[i] Key Type: %s", info.KeyType)
} else if info.KeyType == RESTRICTED {
color.Yellow("[i] Key Type: %s", info.KeyType)
}
if info.KeyEnv == LIVE {
color.Green("[i] Key Environment: %s", info.KeyEnv)
} else if info.KeyEnv == TEST {
color.Red("[i] Key Environment: %s", info.KeyEnv)
}
fmt.Println("")
if info.KeyType == SECRET {
color.Green("[i] Permissions: Full Access")
return
}
printRestrictedPermissions(info.Permissions, cfg.ShowAll)
}
func getRestrictedPermissions(cfg *config.Config, key string) ([]PermissionsCategory, error) {
var config Config
if err := yaml.Unmarshal(restrictedConfig, &config); err != nil {
fmt.Println("Error unmarshalling YAML:", err)
return nil, err
}
output := make([]PermissionsCategory, 0)
for category, scopes := range config.Categories {
permissions := make([]PermissionStruct, 0)
for name, scope := range scopes {
value := ""
testCount := 0
for typ, test := range scope {
if test.Endpoint == "" {
continue
}
testCount++
status, err := test.RunTest(cfg, map[string]string{"Authorization": "Bearer " + key})
if err != nil {
color.Red("[x] Error running test: %s", err.Error())
return nil, err
}
if status {
value = typ
}
if value == "Write" {
break
}
}
if testCount > 0 {
permissions = append(permissions, PermissionStruct{Name: name, Value: &value})
}
}
output = append(output, PermissionsCategory{Name: category, Permissions: permissions})
}
// sort the output
order := []string{"Core", "Checkout", "Billing", "Connect", "Orders", "Issuing", "Reporting", "Identity", "Webhook", "Stripe CLI", "Payment Links", "Terminal", "Tax", "Radar", "Climate"}
// ToDo: order the permissions within each category
// Create a map for quick lookup of the order
orderMap := make(map[string]int)
for i, name := range order {
orderMap[name] = i
}
// Sort the categories according to the desired order
sort.Slice(output, func(i, j int) bool {
return orderMap[output[i].Name] < orderMap[output[j].Name]
})
return output, nil
}
func printRestrictedPermissions(permissions []PermissionsCategory, show_all bool) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Category", "Permission", "Access"})
for _, category := range permissions {
for _, permission := range category.Permissions {
if *permission.Value != "" || show_all {
t.AppendRow([]interface{}{category.Name, permission.Name, *permission.Value})
}
}
}
t.Render()
}
================================================
FILE: pkg/analyzer/analyzers/stripe/stripe_test.go
================================================
package stripe
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
want []byte // JSON string
wantErr bool
}{
{
name: "valid Stripe restricted key",
key: testSecrets.MustGetField("STRIPE_SECRET"),
want: expectedOutput,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
================================================
FILE: pkg/analyzer/analyzers/twilio/permissions.go
================================================
// Code generated by go generate; DO NOT EDIT.
package twilio
import "errors"
type Permission int
const (
Invalid Permission = iota
AccountManagementRead Permission = iota
AccountManagementWrite Permission = iota
SubaccountConfigurationRead Permission = iota
SubaccountConfigurationWrite Permission = iota
KeyManagementRead Permission = iota
KeyManagementWrite Permission = iota
ServiceVerificationRead Permission = iota
ServiceVerificationWrite Permission = iota
SmsRead Permission = iota
SmsWrite Permission = iota
VoiceRead Permission = iota
VoiceWrite Permission = iota
MessagingRead Permission = iota
MessagingWrite Permission = iota
CallManagementRead Permission = iota
CallManagementWrite Permission = iota
)
var (
PermissionStrings = map[Permission]string{
AccountManagementRead: "account_management:read",
AccountManagementWrite: "account_management:write",
SubaccountConfigurationRead: "subaccount_configuration:read",
SubaccountConfigurationWrite: "subaccount_configuration:write",
KeyManagementRead: "key_management:read",
KeyManagementWrite: "key_management:write",
ServiceVerificationRead: "service_verification:read",
ServiceVerificationWrite: "service_verification:write",
SmsRead: "sms:read",
SmsWrite: "sms:write",
VoiceRead: "voice:read",
VoiceWrite: "voice:write",
MessagingRead: "messaging:read",
MessagingWrite: "messaging:write",
CallManagementRead: "call_management:read",
CallManagementWrite: "call_management:write",
}
StringToPermission = map[string]Permission{
"account_management:read": AccountManagementRead,
"account_management:write": AccountManagementWrite,
"subaccount_configuration:read": SubaccountConfigurationRead,
"subaccount_configuration:write": SubaccountConfigurationWrite,
"key_management:read": KeyManagementRead,
"key_management:write": KeyManagementWrite,
"service_verification:read": ServiceVerificationRead,
"service_verification:write": ServiceVerificationWrite,
"sms:read": SmsRead,
"sms:write": SmsWrite,
"voice:read": VoiceRead,
"voice:write": VoiceWrite,
"messaging:read": MessagingRead,
"messaging:write": MessagingWrite,
"call_management:read": CallManagementRead,
"call_management:write": CallManagementWrite,
}
PermissionIDs = map[Permission]int{
AccountManagementRead: 1,
AccountManagementWrite: 2,
SubaccountConfigurationRead: 3,
SubaccountConfigurationWrite: 4,
KeyManagementRead: 5,
KeyManagementWrite: 6,
ServiceVerificationRead: 7,
ServiceVerificationWrite: 8,
SmsRead: 9,
SmsWrite: 10,
VoiceRead: 11,
VoiceWrite: 12,
MessagingRead: 13,
MessagingWrite: 14,
CallManagementRead: 15,
CallManagementWrite: 16,
}
IdToPermission = map[int]Permission{
1: AccountManagementRead,
2: AccountManagementWrite,
3: SubaccountConfigurationRead,
4: SubaccountConfigurationWrite,
5: KeyManagementRead,
6: KeyManagementWrite,
7: ServiceVerificationRead,
8: ServiceVerificationWrite,
9: SmsRead,
10: SmsWrite,
11: VoiceRead,
12: VoiceWrite,
13: MessagingRead,
14: MessagingWrite,
15: CallManagementRead,
16: CallManagementWrite,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
================================================
FILE: pkg/analyzer/analyzers/twilio/permissions.yaml
================================================
permissions:
- account_management:read
- account_management:write
- subaccount_configuration:read
- subaccount_configuration:write
- key_management:read
- key_management:write
- service_verification:read
- service_verification:write
- sms:read
- sms:write
- voice:read
- voice:write
- messaging:read
- messaging:write
- call_management:read
- call_management:write
================================================
FILE: pkg/analyzer/analyzers/twilio/twilio.go
================================================
//go:generate generate_permissions permissions.yaml permissions.go twilio
package twilio
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/fatih/color"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
type Analyzer struct {
Cfg *config.Config
}
func (a *Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeTwilio
}
func (a *Analyzer) Analyze(ctx context.Context, credentialInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credentialInfo["key"]
if !ok {
return nil, errors.New("key not found in credentialInfo")
}
sid, ok := credentialInfo["sid"]
if !ok {
return nil, errors.New("sid not found in credentialInfo")
}
if a.Cfg == nil {
a.Cfg = &config.Config{} // You might need to adjust this based on how you want to handle config
}
info, err := AnalyzePermissions(a.Cfg, sid, key)
if err != nil {
return nil, err
}
// List parent and subaccounts
accounts, err := listTwilioAccounts(a.Cfg, sid, key)
if err != nil {
return nil, err
}
var permissions []Permission
if info.AccountStatusCode == 200 {
permissions = []Permission{
AccountManagementRead,
AccountManagementWrite,
SubaccountConfigurationRead,
SubaccountConfigurationWrite,
KeyManagementRead,
KeyManagementWrite,
ServiceVerificationRead,
ServiceVerificationWrite,
SmsRead,
SmsWrite,
VoiceRead,
VoiceWrite,
MessagingRead,
MessagingWrite,
CallManagementRead,
CallManagementWrite,
}
} else if info.AccountStatusCode == 401 {
permissions = []Permission{
ServiceVerificationRead,
ServiceVerificationWrite,
SmsRead,
SmsWrite,
VoiceRead,
VoiceWrite,
MessagingRead,
MessagingWrite,
CallManagementRead,
CallManagementWrite,
}
}
var (
bindings []analyzers.Binding
parentAccountSID = ""
parentAccountFriendlyName = ""
)
if len(info.ServicesRes.Services) > 0 {
parentAccountSID = info.ServicesRes.Services[0].AccountSID
parentAccountFriendlyName = info.ServicesRes.Services[0].FriendlyName
}
for _, account := range accounts {
accountType := "Account"
if parentAccountSID != "" && account.SID != parentAccountSID {
accountType = "SubAccount"
}
resource := analyzers.Resource{
Name: account.FriendlyName,
FullyQualifiedName: "twilio.com/account/" + account.SID,
Type: accountType,
}
if parentAccountSID != "" && account.SID != parentAccountSID {
resource.Parent = &analyzers.Resource{
Name: parentAccountFriendlyName,
FullyQualifiedName: "twilio.com/account/" + parentAccountSID,
Type: "Account",
}
}
for _, perm := range permissions {
permStr, _ := perm.ToString()
bindings = append(bindings, analyzers.Binding{
Resource: resource,
Permission: analyzers.Permission{
Value: permStr,
},
})
}
}
return &analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeTwilio,
Bindings: bindings,
}, nil
}
type secretInfo struct {
ServicesRes serviceResponse
AccountStatusCode int
}
const (
AUTHENTICATED_NO_PERMISSION = 70051
INVALID_CREDENTIALS = 20003
)
// getAccountsStatusCode returns the status code from the Accounts endpoint
// this is used to determine whether the key is scoped as main or standard, since standard has no access here.
func getAccountsStatusCode(cfg *config.Config, sid string, secret string) (int, error) {
// create http client
client := analyzers.NewAnalyzeClient(cfg)
// create request
req, err := http.NewRequest("GET", "https://api.twilio.com/2010-04-01/Accounts", nil)
if err != nil {
return 0, err
}
// add basicAuth
req.SetBasicAuth(sid, secret)
// send request
resp, err := client.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
return resp.StatusCode, nil
}
type serviceResponse struct {
Code int `json:"code"`
Services []service `json:"services"`
}
type service struct {
FriendlyName string `json:"friendly_name"` // friendly name of a service
SID string `json:"sid"` // object id of service
AccountSID string `json:"account_sid"` // account sid
}
// getVerifyServicesStatusCode returns the status code and the JSON response from the Verify Services endpoint
// only the code value is captured in the JSON response and this is only shown when the key is invalid or has no permissions
func getVerifyServicesStatusCode(cfg *config.Config, sid string, secret string) (serviceResponse, error) {
var serviceRes serviceResponse
// create http client
client := analyzers.NewAnalyzeClient(cfg)
// create request
req, err := http.NewRequest("GET", "https://verify.twilio.com/v2/Services", nil)
if err != nil {
return serviceRes, err
}
// add basicAuth
req.SetBasicAuth(sid, secret)
// send request
resp, err := client.Do(req)
if err != nil {
return serviceRes, err
}
defer resp.Body.Close()
// read response
if err := json.NewDecoder(resp.Body).Decode(&serviceRes); err != nil {
return serviceRes, err
}
return serviceRes, nil
}
func listTwilioAccounts(cfg *config.Config, sid, secret string) ([]service, error) {
// create http client
client := analyzers.NewAnalyzeClient(cfg)
// create request
req, err := http.NewRequest("GET", "https://api.twilio.com/2010-04-01/Accounts.json", nil)
if err != nil {
return nil, err
}
// add basicAuth
req.SetBasicAuth(sid, secret)
// send request
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result struct {
Accounts []service `json:"accounts"`
}
// read response
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result.Accounts, nil
}
func AnalyzePermissions(cfg *config.Config, sid, secret string) (*secretInfo, error) {
servicesRes, err := getVerifyServicesStatusCode(cfg, sid, secret)
if err != nil {
return nil, err
}
statusCode, err := getAccountsStatusCode(cfg, sid, secret)
if err != nil {
return nil, err
}
return &secretInfo{
ServicesRes: servicesRes,
AccountStatusCode: statusCode,
}, nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, sid, secret string) {
info, err := AnalyzePermissions(cfg, sid, secret)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
if info.ServicesRes.Code == INVALID_CREDENTIALS {
color.Red("[x] Invalid Twilio API Key")
return
}
if info.ServicesRes.Code == AUTHENTICATED_NO_PERMISSION {
printRestrictedKeyMsg()
return
}
printPermissions(info.AccountStatusCode)
}
// printPermissions prints the permissions based on the status code
// 200 means the key is main, 401 means the key is standard
func printPermissions(statusCode int) {
if statusCode != 200 && statusCode != 401 {
color.Red("[x] Invalid Twilio API Key")
return
}
color.Green("[!] Valid Twilio API Key\n")
color.Green("[i] Expires: Never")
if statusCode == 401 {
color.Yellow("[i] Key type: Standard")
color.Yellow("[i] Permissions: All EXCEPT key management and account/subaccount configuration.")
} else if statusCode == 200 {
color.Green("[i] Key type: Main (aka Admin)")
color.Green("[i] Permissions: All")
}
}
// printRestrictedKeyMsg prints the message for a restricted key
// this is a temporary measure since the restricted key type is still in beta
func printRestrictedKeyMsg() {
color.Green("[!] Valid Twilio API Key\n")
color.Green("[i] Expires: Never")
color.Yellow("[i] Key type: Restricted")
color.Yellow("[i] Permissions: Limited")
fmt.Println("[*] Note: Twilio is rolling out a Restricted API Key type, which provides fine-grained control over API endpoints. Since it's still in a Public Beta, this has not been incorporated into this tool.")
}
================================================
FILE: pkg/analyzer/analyzers/twilio/twilio_test.go
================================================
package twilio
import (
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
sid string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid Twilio key",
sid: testSecrets.MustGetField("TWILLIO_ID"),
key: testSecrets.MustGetField("TWILLIO_API"),
want: ` {
"AnalyzerType": 20,
"Bindings": [
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "account_management:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "account_management:write",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "subaccount_configuration:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "subaccount_configuration:write",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "key_management:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "key_management:write",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "service_verification:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "service_verification:write",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "sms:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "sms:write",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "voice:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "voice:write",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "messaging:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "messaging:write",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "call_management:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "call_management:write",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": null
}`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key, "sid": tt.sid})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = \n%s", gotIndented)
}
})
}
}
================================================
FILE: pkg/analyzer/cli.go
================================================
package analyzer
import (
"strings"
"github.com/alecthomas/kingpin/v2"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/airbrake"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/airtable/airtableoauth"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/airtable/airtablepat"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/anthropic"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/asana"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/bitbucket"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/databricks"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/datadog"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/digitalocean"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/dockerhub"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/dropbox"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/elevenlabs"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/fastly"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/figma"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/gitlab"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/groq"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/huggingface"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/jira"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/launchdarkly"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mailchimp"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mailgun"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/monday"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mux"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mysql"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/netlify"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/ngrok"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/notion"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/openai"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/opsgenie"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/plaid"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/planetscale"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/postgres"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/posthog"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/postman"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/privatekey"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/sendgrid"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/shopify"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/slack"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/sourcegraph"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/square"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/stripe"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/twilio"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
type SecretInfo struct {
Parts map[string]string
Cfg *config.Config
}
func Command(app *kingpin.Application) *kingpin.CmdClause {
return app.Command("analyze", "Analyze API keys for fine-grained permissions information.")
}
func Run(keyType string, secretInfo SecretInfo) {
if secretInfo.Cfg == nil {
secretInfo.Cfg = &config.Config{}
}
switch strings.ToLower(keyType) {
case "github":
github.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "sendgrid":
sendgrid.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "openai":
openai.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "postgres":
postgres.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "mysql":
mysql.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "slack":
slack.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "twilio":
twilio.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["sid"], secretInfo.Parts["key"])
case "airbrake":
airbrake.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "huggingface":
huggingface.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "stripe":
stripe.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "gitlab":
gitlab.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "mailchimp":
mailchimp.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "postman":
postman.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "bitbucket":
bitbucket.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "asana":
asana.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "mailgun":
mailgun.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "square":
square.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "sourcegraph":
sourcegraph.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "shopify":
shopify.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"], secretInfo.Parts["url"])
case "opsgenie":
opsgenie.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "privatekey":
privatekey.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "notion":
notion.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "dockerhub":
dockerhub.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["username"], secretInfo.Parts["pat"])
case "anthropic":
anthropic.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "digitalocean":
digitalocean.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "elevenlabs":
elevenlabs.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "planetscale":
planetscale.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["id"], secretInfo.Parts["token"])
case "airtableoauth":
airtableoauth.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "airtablepat":
airtablepat.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "groq":
groq.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "launchdarkly":
launchdarkly.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "figma":
figma.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "plaid":
plaid.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["secret"], secretInfo.Parts["id"], secretInfo.Parts["token"])
case "netlify":
netlify.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "fastly":
fastly.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "monday":
monday.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "datadog":
datadog.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["api_key"], secretInfo.Parts["app_key"], secretInfo.Parts["endpoint"])
case "ngrok":
ngrok.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "mux":
mux.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"], secretInfo.Parts["secret"])
case "posthog":
posthog.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "dropbox":
dropbox.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "databricks":
databricks.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["domain"], secretInfo.Parts["token"])
case "jira":
jira.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["domain"], secretInfo.Parts["email"], secretInfo.Parts["token"])
}
}
================================================
FILE: pkg/analyzer/config/config.go
================================================
package config
// TODO: separate CLI configuration from analysis configuration.
type Config struct {
LoggingEnabled bool
LogFile string
ShowAll bool
// Limit API calls when enumerating permissions.
Shallow bool
}
================================================
FILE: pkg/analyzer/generate_permissions/generate_permissions.go
================================================
package main
import (
"fmt"
"log"
"os"
"regexp"
"strings"
"text/template"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gopkg.in/yaml.v3"
)
type PermissionsData struct {
Permissions []string `yaml:"permissions"`
PackageName string `yaml:"package_name"`
}
const templateText = `// Code generated by go generate; DO NOT EDIT.
package {{ .PackageName }}
import "errors"
type Permission int
const (
Invalid Permission = iota
{{- range $index, $permission := .Permissions }}
{{ ToCamelCase $permission }} Permission = iota
{{- end }}
)
var (
PermissionStrings = map[Permission]string{
{{- range $index, $permission := .Permissions }}
{{ ToCamelCase $permission }}: "{{ $permission }}",
{{- end }}
}
StringToPermission = map[string]Permission{
{{- range $index, $permission := .Permissions }}
"{{ $permission }}": {{ ToCamelCase $permission }},
{{- end }}
}
PermissionIDs = map[Permission]int{
{{- range $index, $permission := .Permissions }}
{{ ToCamelCase $permission }}: {{ inc $index }},
{{- end }}
}
IdToPermission = map[int]Permission{
{{- range $index, $permission := .Permissions }}
{{ inc $index }}: {{ ToCamelCase $permission }},
{{- end }}
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
`
// ToCamelCase converts a string to CamelCase
func ToCamelCase(s string) string {
parts := strings.Split(s, ":")
caser := cases.Title(language.English)
for i := range parts {
subParts := regexp.MustCompile(`[\_\.\-]+`).Split(parts[i], -1)
for j := range subParts {
subParts[j] = caser.String(subParts[j])
}
parts[i] = strings.Join(subParts, "")
}
return strings.Join(parts, "")
}
func main() {
// Read the YAML file from first argument
file, err := os.Open(os.Args[1])
if err != nil {
log.Fatalf("Failed to open YAML file: %v", err)
}
defer file.Close()
var data PermissionsData
decoder := yaml.NewDecoder(file)
err = decoder.Decode(&data)
if err != nil {
log.Fatalf("Failed to decode YAML file: %v", err)
}
data.PackageName = os.Args[3]
// Parse the template
tmpl, err := template.New("permissions").Funcs(template.FuncMap{
"ToCamelCase": ToCamelCase,
"inc": func(i int) int { return i + 1 },
}).Parse(templateText)
if err != nil {
log.Fatalf("Failed to parse template: %v", err)
}
// Generate the code
outputFile, err := os.Create(os.Args[2])
if err != nil {
log.Fatalf("Failed to create output file: %v", err)
}
defer outputFile.Close()
err = tmpl.Execute(outputFile, data)
if err != nil {
log.Fatalf("Failed to execute template: %v", err)
}
fmt.Println("Permissions code generated successfully.")
}
================================================
FILE: pkg/buffers/buffer/buffer.go
================================================
// Package buffer provides a custom buffer type that includes metrics for tracking buffer usage.
// It also provides a pool for managing buffer reusability.
package buffer
import (
"bytes"
"io"
"time"
)
// Buffer is a wrapper around bytes.Buffer that includes a timestamp for tracking Buffer checkout duration.
type Buffer struct {
*bytes.Buffer
checkedOutAt time.Time
}
const defaultBufferSize = 1 << 12 // 4KB
// NewBuffer creates a new instance of Buffer.
func NewBuffer() *Buffer { return &Buffer{Buffer: bytes.NewBuffer(make([]byte, 0, defaultBufferSize))} }
func (b *Buffer) Grow(size int) {
b.Buffer.Grow(size)
b.recordGrowth(size)
}
func (b *Buffer) ResetMetric() { b.checkedOutAt = time.Now() }
func (b *Buffer) RecordMetric() {
dur := time.Since(b.checkedOutAt)
checkoutDuration.Observe(float64(dur.Microseconds()))
checkoutDurationTotal.Add(float64(dur.Microseconds()))
totalBufferSize.Add(float64(b.Cap()))
totalBufferLength.Add(float64(b.Len()))
}
func (b *Buffer) recordGrowth(size int) {
growCount.Inc()
growAmount.Add(float64(size))
}
// Write date to the buffer.
func (b *Buffer) Write(data []byte) (int, error) {
if b.Buffer == nil {
// This case should ideally never occur if buffers are properly managed.
b.Buffer = bytes.NewBuffer(make([]byte, 0, defaultBufferSize))
b.ResetMetric()
}
size := len(data)
bufferLength := b.Buffer.Len()
totalSizeNeeded := bufferLength + size
// If the total size is within the threshold, write to the buffer.
availableSpace := b.Buffer.Cap() - bufferLength
growSize := totalSizeNeeded - bufferLength
if growSize > availableSpace {
// We are manually growing the buffer so we can track the growth via metrics.
// Knowing the exact data size, we directly resize to fit it, rather than exponential growth
// which may require multiple allocations and copies if the size required is much larger
// than double the capacity. Our approach aligns with default behavior when growth sizes
// happen to match current capacity, retaining asymptotic efficiency benefits.
b.Grow(growSize)
}
return b.Buffer.Write(data)
}
// Compile time check to make sure readCloser implements io.ReadSeekCloser.
var _ io.ReadSeekCloser = (*readCloser)(nil)
// readCloser is a custom implementation of io.ReadCloser. It wraps a bytes.Reader
// for reading data from an in-memory buffer and includes an onClose callback.
// The onClose callback is used to return the buffer to the pool, ensuring buffer re-usability.
type readCloser struct {
*bytes.Reader
onClose func()
}
// ReadCloser creates a new instance of readCloser.
func ReadCloser(data []byte, onClose func()) *readCloser {
return &readCloser{Reader: bytes.NewReader(data), onClose: onClose}
}
// Close implements the io.Closer interface. It calls the onClose callback to return the buffer
// to the pool, enabling buffer reuse. This method should be called by the consumers of ReadCloser
// once they have finished reading the data to ensure proper resource management.
func (brc *readCloser) Close() error {
if brc.onClose == nil {
return nil
}
brc.onClose() // Return the buffer to the pool
brc.Reader = nil
return nil
}
// Read reads up to len(p) bytes into p from the underlying reader.
// It returns the number of bytes read and any error encountered.
// On reaching the end of the available data, it returns 0 and io.EOF.
// Calling Read on a closed reader will also return 0 and io.EOF.
func (brc *readCloser) Read(p []byte) (int, error) {
if brc.Reader == nil {
return 0, io.EOF
}
return brc.Reader.Read(p)
}
================================================
FILE: pkg/buffers/buffer/buffer_test.go
================================================
package buffer
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestBufferWrite(t *testing.T) {
t.Parallel()
tests := []struct {
name string
initialCapacity int
writeDataSequence [][]byte // Sequence of writes to simulate multiple writes
expectedSize int
expectedCap int
}{
{
name: "Write to empty buffer",
initialCapacity: defaultBufferSize,
writeDataSequence: [][]byte{
[]byte("hello"),
},
expectedSize: 5,
expectedCap: defaultBufferSize, // No growth for small data
},
{
name: "Write causing growth",
initialCapacity: 10, // Small initial capacity to force growth
writeDataSequence: [][]byte{
[]byte("this is a longer string exceeding initial capacity"),
},
expectedSize: 50,
expectedCap: 50,
},
{
name: "Write nil data",
initialCapacity: defaultBufferSize,
writeDataSequence: [][]byte{nil},
expectedCap: defaultBufferSize,
},
{
name: "Repeated writes, cumulative growth",
initialCapacity: 20, // Set an initial capacity to test growth over multiple writes
writeDataSequence: [][]byte{
[]byte("first write, "),
[]byte("second write, "),
[]byte("third write exceeding the initial capacity."),
},
expectedSize: 70,
expectedCap: 70, // Expect capacity to grow to accommodate all writes
},
{
name: "Write large single data to test significant growth",
initialCapacity: 50, // Set an initial capacity smaller than the data to be written
writeDataSequence: [][]byte{
bytes.Repeat([]byte("a"), 1024), // 1KB data to significantly exceed initial capacity
},
expectedSize: 1024,
expectedCap: 1024,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
buf := &Buffer{Buffer: bytes.NewBuffer(make([]byte, 0, tc.initialCapacity))}
totalWritten := 0
for _, data := range tc.writeDataSequence {
n, err := buf.Write(data)
assert.NoError(t, err)
totalWritten += n
}
assert.Equal(t, tc.expectedSize, totalWritten)
assert.Equal(t, tc.expectedSize, buf.Len())
assert.GreaterOrEqual(t, buf.Cap(), tc.expectedCap)
})
}
}
func TestReadCloserClose(t *testing.T) {
t.Parallel()
onCloseCalled := false
rc := ReadCloser([]byte("data"), func() { onCloseCalled = true })
err := rc.Close()
assert.NoError(t, err)
assert.True(t, onCloseCalled, "onClose callback should be called upon Close")
}
================================================
FILE: pkg/buffers/buffer/metrics.go
================================================
package buffer
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
)
var (
growCount = promauto.NewCounter(prometheus.CounterOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "grow_count",
Help: "Total number of times buffers in the pool have grown.",
})
growAmount = promauto.NewCounter(prometheus.CounterOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "grow_amount",
Help: "Total amount of bytes buffers in the pool have grown by.",
})
checkoutDurationTotal = promauto.NewCounter(prometheus.CounterOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "checkout_duration_total_us",
Help: "Total duration in microseconds of Buffer checkouts.",
})
checkoutDuration = promauto.NewHistogram(prometheus.HistogramOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "checkout_duration_us",
Help: "Duration in microseconds of Buffer checkouts.",
Buckets: prometheus.ExponentialBuckets(10, 10, 7),
})
totalBufferLength = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "total_buffer_length",
Help: "Total length of all buffers combined.",
})
totalBufferSize = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "total_buffer_size",
Help: "Total size of all buffers combined.",
})
)
================================================
FILE: pkg/buffers/pool/metrics.go
================================================
package pool
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
)
var (
activeBufferCount = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "active_buffer_count",
Help: "Current number of active buffers.",
})
bufferCount = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "buffer_count",
Help: "Total number of buffers managed by the pool.",
})
shrinkCount = promauto.NewCounter(prometheus.CounterOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "shrink_count",
Help: "Total number of times buffers in the pool have shrunk.",
})
shrinkAmount = promauto.NewCounter(prometheus.CounterOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "shrink_amount",
Help: "Total amount of bytes buffers in the pool have shrunk by.",
})
checkoutCount = promauto.NewCounter(prometheus.CounterOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "checkout_count",
Help: "Total number of Buffer checkouts.",
})
)
================================================
FILE: pkg/buffers/pool/pool.go
================================================
package pool
import (
"bytes"
"sync"
"github.com/trufflesecurity/trufflehog/v3/pkg/buffers/buffer"
)
type poolMetrics struct{}
func (poolMetrics) recordShrink(amount int) {
shrinkCount.Inc()
shrinkAmount.Add(float64(amount))
}
func (poolMetrics) recordBufferRetrival() {
activeBufferCount.Inc()
checkoutCount.Inc()
bufferCount.Inc()
}
func (poolMetrics) recordBufferReturn(buf *buffer.Buffer) {
activeBufferCount.Dec()
buf.RecordMetric()
}
// Pool of buffers.
type Pool struct {
*sync.Pool
bufferSize int
metrics poolMetrics
}
const defaultBufferSize = 1 << 12 // 4KB
// NewBufferPool creates a new instance of BufferPool.
func NewBufferPool(size int) *Pool {
pool := &Pool{bufferSize: size}
pool.Pool = &sync.Pool{
New: func() any {
return &buffer.Buffer{Buffer: bytes.NewBuffer(make([]byte, 0, pool.bufferSize))}
},
}
return pool
}
// Get returns a Buffer from the pool.
func (p *Pool) Get() *buffer.Buffer {
buf, ok := p.Pool.Get().(*buffer.Buffer)
if !ok {
buf = &buffer.Buffer{Buffer: bytes.NewBuffer(make([]byte, 0, p.bufferSize))}
}
p.metrics.recordBufferRetrival()
buf.ResetMetric()
return buf
}
// Put returns a Buffer to the pool.
func (p *Pool) Put(buf *buffer.Buffer) {
p.metrics.recordBufferReturn(buf)
// If the Buffer is more than twice the default size, replace it with a new Buffer.
// This prevents us from returning very large buffers to the pool.
const maxAllowedCapacity = 2 * defaultBufferSize
if buf.Cap() > int(maxAllowedCapacity) {
p.metrics.recordShrink(buf.Cap() - defaultBufferSize)
buf = &buffer.Buffer{Buffer: bytes.NewBuffer(make([]byte, 0, p.bufferSize))}
} else {
// Reset the Buffer to clear any existing data.
buf.Reset()
}
p.Pool.Put(buf)
}
================================================
FILE: pkg/buffers/pool/pool_test.go
================================================
package pool
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/trufflesecurity/trufflehog/v3/pkg/buffers/buffer"
)
func TestNewBufferPool(t *testing.T) {
t.Parallel()
tests := []struct {
name string
size int
expectedBuffSize int
}{
{name: "Default pool size", size: defaultBufferSize, expectedBuffSize: defaultBufferSize},
{
name: "Custom pool size",
size: 8 * 1024,
expectedBuffSize: 8 * 1024,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
pool := NewBufferPool(tc.size)
assert.Equal(t, tc.expectedBuffSize, pool.bufferSize)
})
}
}
func TestBufferPoolGetPut(t *testing.T) {
t.Parallel()
tests := []struct {
name string
preparePool func(p *Pool) *buffer.Buffer // Prepare the pool and return an initial buffer to put if needed
expectedCapBefore int // Expected capacity before putting it back
expectedCapAfter int // Expected capacity after retrieving it again
}{
{
name: "Get new buffer and put back without modification",
preparePool: func(_ *Pool) *buffer.Buffer {
return nil // No initial buffer to put
},
expectedCapBefore: defaultBufferSize,
expectedCapAfter: defaultBufferSize,
},
{
name: "Put oversized buffer, expect shrink",
preparePool: func(p *Pool) *buffer.Buffer {
buf := &buffer.Buffer{Buffer: bytes.NewBuffer(make([]byte, 0, 3*defaultBufferSize))}
return buf
},
expectedCapBefore: defaultBufferSize,
expectedCapAfter: defaultBufferSize, // Should shrink back to default
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
pool := NewBufferPool(defaultBufferSize)
initialBuf := tc.preparePool(pool)
if initialBuf != nil {
pool.Put(initialBuf)
}
buf := pool.Get()
assert.Equal(t, tc.expectedCapBefore, buf.Cap())
pool.Put(buf)
bufAfter := pool.Get()
assert.Equal(t, tc.expectedCapAfter, bufAfter.Cap())
})
}
}
================================================
FILE: pkg/cache/cache.go
================================================
// Package cache provides an interface which can be implemented by different cache types.
package cache
// Cache is used to store key/value pairs.
type Cache[T any] interface {
// Set stores the given key/value pair.
Set(key string, val T)
// Get returns the value for the given key and a boolean indicating if the key was found.
Get(key string) (T, bool)
// Exists returns true if the given key exists in the cache.
Exists(key string) bool
// Delete the given key from the cache.
Delete(key string)
// Clear all key/value pairs from the cache.
Clear()
// Count the number of key/value pairs in the cache.
Count() int
// Keys returns all keys in the cache.
Keys() []string
// Values returns all values in the cache.
Values() []T
// Contents returns all keys in the cache encoded as a string.
Contents() string
}
================================================
FILE: pkg/cache/decorator.go
================================================
package cache
// WithMetrics is a decorator that adds metrics collection to any Cache implementation.
type WithMetrics[T any] struct {
wrapped Cache[T]
metrics BaseMetricsCollector
cacheName string
}
// NewCacheWithMetrics creates a new WithMetrics decorator that wraps the provided Cache
// and collects metrics using the provided BaseMetricsCollector.
// The cacheName parameter is used to identify the cache in the collected metrics.
func NewCacheWithMetrics[T any](wrapped Cache[T], metrics BaseMetricsCollector, cacheName string) *WithMetrics[T] {
return &WithMetrics[T]{
wrapped: wrapped,
metrics: metrics,
cacheName: cacheName,
}
}
// Set sets the value for the given key in the cache. It also records a set metric
// for the cache using the provided metrics collector and cache name.
func (c *WithMetrics[T]) Set(key string, val T) {
c.metrics.RecordSet(c.cacheName)
c.wrapped.Set(key, val)
}
// Get retrieves the value for the given key from the underlying cache. It also records
// a hit or miss metric for the cache using the provided metrics collector and cache name.
func (c *WithMetrics[T]) Get(key string) (T, bool) {
val, found := c.wrapped.Get(key)
if found {
c.metrics.RecordHit(c.cacheName)
} else {
c.metrics.RecordMiss(c.cacheName)
}
return val, found
}
// Exists checks if the given key exists in the cache. It records a hit or miss metric
// for the cache using the provided metrics collector and cache name.
func (c *WithMetrics[T]) Exists(key string) bool {
found := c.wrapped.Exists(key)
if found {
c.metrics.RecordHit(c.cacheName)
} else {
c.metrics.RecordMiss(c.cacheName)
}
return found
}
// Delete removes the value for the given key from the cache. It also records a delete metric
// for the cache using the provided metrics collector and cache name.
func (c *WithMetrics[T]) Delete(key string) {
c.wrapped.Delete(key)
c.metrics.RecordDelete(c.cacheName)
}
// Clear removes all entries from the cache. It also records a clear metric
// for the cache using the provided metrics collector and cache name.
func (c *WithMetrics[T]) Clear() {
c.wrapped.Clear()
c.metrics.RecordClear(c.cacheName)
}
// Count returns the number of entries in the cache. It also records a count metric
// for the cache using the provided metrics collector and cache name.
func (c *WithMetrics[T]) Count() int {
count := c.wrapped.Count()
return count
}
// Keys returns all keys in the cache. It also records a keys metric
// for the cache using the provided metrics collector and cache name.
func (c *WithMetrics[T]) Keys() []string { return c.wrapped.Keys() }
// Values returns all values in the cache. It also records a values metric
// for the cache using the provided metrics collector and cache name.
func (c *WithMetrics[T]) Values() []T { return c.wrapped.Values() }
// Contents returns all keys in the cache as a string. It also records a contents metric
// for the cache using the provided metrics collector and cache name.
func (c *WithMetrics[T]) Contents() string { return c.wrapped.Contents() }
================================================
FILE: pkg/cache/decorator_test.go
================================================
package cache
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type mockCollector struct{ mock.Mock }
func (m *mockCollector) RecordHits(cacheName string, hits uint64) { m.Called(cacheName, hits) }
func (m *mockCollector) RecordMisses(cacheName string, misses uint64) { m.Called(cacheName, misses) }
func (m *mockCollector) RecordSet(cacheName string) { m.Called(cacheName) }
func (m *mockCollector) RecordHit(cacheName string) { m.Called(cacheName) }
func (m *mockCollector) RecordMiss(cacheName string) { m.Called(cacheName) }
func (m *mockCollector) RecordDelete(cacheName string) { m.Called(cacheName) }
func (m *mockCollector) RecordClear(cacheName string) { m.Called(cacheName) }
type mockCache[T any] struct{ mock.Mock }
func (m *mockCache[T]) Set(key string, val T) { m.Called(key, val) }
func (m *mockCache[T]) Get(key string) (T, bool) {
args := m.Called(key)
var zero T
if args.Get(0) != nil {
return args.Get(0).(T), args.Bool(1)
}
return zero, args.Bool(1)
}
func (m *mockCache[T]) Exists(key string) bool {
args := m.Called(key)
return args.Bool(0)
}
func (m *mockCache[T]) Delete(key string) { m.Called(key) }
func (m *mockCache[T]) Clear() { m.Called() }
func (m *mockCache[T]) Count() int {
args := m.Called()
return args.Int(0)
}
func (m *mockCache[T]) Keys() []string {
args := m.Called()
return args.Get(0).([]string)
}
func (m *mockCache[T]) Values() []T {
args := m.Called()
return args.Get(0).([]T)
}
func (m *mockCache[T]) Contents() string {
args := m.Called()
return args.String(0)
}
// setupCache initializes the mock cache and metrics collector, then wraps them with the WithMetrics decorator.
func setupCache[T any](t *testing.T) (*WithMetrics[T], *mockCache[T], *mockCollector) {
t.Helper()
collector := new(mockCollector)
cache := new(mockCache[T])
wrappedCache := NewCacheWithMetrics[T](cache, collector, "test_cache")
assert.NotNil(t, wrappedCache, "WithMetrics cache should not be nil")
return wrappedCache, cache, collector
}
func TestNewLRUCache(t *testing.T) {
c, _, _ := setupCache[int](t)
assert.Equal(t, "test_cache", c.cacheName)
}
func TestCacheSet(t *testing.T) {
c, cacheMock, collectorMock := setupCache[string](t)
collectorMock.On("RecordSet", "test_cache").Once()
cacheMock.On("Set", "key", "value").Once()
c.Set("key", "value")
collectorMock.AssertCalled(t, "RecordSet", "test_cache")
cacheMock.AssertCalled(t, "Set", "key", "value")
}
func TestCacheGet(t *testing.T) {
c, cacheMock, collectorMock := setupCache[string](t)
collectorMock.On("RecordSet", "test_cache").Once()
cacheMock.On("Set", "key", "value").Once()
collectorMock.On("RecordHit", "test_cache").Once()
cacheMock.On("Get", "key").Return("value", true).Once()
collectorMock.On("RecordMiss", "test_cache").Once()
cacheMock.On("Get", "non_existent").Return("", false).Once()
c.Set("key", "value")
collectorMock.AssertCalled(t, "RecordSet", "test_cache")
cacheMock.AssertCalled(t, "Set", "key", "value")
value, found := c.Get("key")
assert.True(t, found, "Expected to find the key")
assert.Equal(t, "value", value, "Expected value to match")
collectorMock.AssertCalled(t, "RecordHit", "test_cache")
cacheMock.AssertCalled(t, "Get", "key")
_, found = c.Get("non_existent")
assert.False(t, found, "Expected not to find the key")
collectorMock.AssertCalled(t, "RecordMiss", "test_cache")
cacheMock.AssertCalled(t, "Get", "non_existent")
collectorMock.AssertExpectations(t)
cacheMock.AssertExpectations(t)
}
func TestCacheExists(t *testing.T) {
c, cacheMock, collectorMock := setupCache[string](t)
collectorMock.On("RecordSet", "test_cache").Once()
cacheMock.On("Set", "key", "value").Once()
collectorMock.On("RecordHit", "test_cache").Once()
cacheMock.On("Exists", "key").Return(true).Once()
collectorMock.On("RecordMiss", "test_cache").Once()
cacheMock.On("Exists", "non_existent").Return(false).Once()
c.Set("key", "value")
collectorMock.AssertCalled(t, "RecordSet", "test_cache")
cacheMock.AssertCalled(t, "Set", "key", "value")
exists := c.Exists("key")
assert.True(t, exists, "Expected the key to exist")
collectorMock.AssertCalled(t, "RecordHit", "test_cache")
cacheMock.AssertCalled(t, "Exists", "key")
exists = c.Exists("non_existent")
assert.False(t, exists, "Expected the key not to exist")
collectorMock.AssertCalled(t, "RecordMiss", "test_cache")
cacheMock.AssertCalled(t, "Exists", "non_existent")
collectorMock.AssertExpectations(t)
cacheMock.AssertExpectations(t)
}
func TestCacheDelete(t *testing.T) {
c, cacheMock, collectorMock := setupCache[string](t)
collectorMock.On("RecordSet", "test_cache").Once()
cacheMock.On("Set", "key", "value").Once()
collectorMock.On("RecordDelete", "test_cache").Once()
cacheMock.On("Delete", "key").Once()
cacheMock.On("Get", "key").Return("", false).Once()
collectorMock.On("RecordMiss", "test_cache").Once()
c.Set("key", "value")
collectorMock.AssertCalled(t, "RecordSet", "test_cache")
cacheMock.AssertCalled(t, "Set", "key", "value")
c.Delete("key")
collectorMock.AssertCalled(t, "RecordDelete", "test_cache")
cacheMock.AssertCalled(t, "Delete", "key")
_, found := c.Get("key")
assert.False(t, found, "Expected not to find the deleted key")
collectorMock.AssertCalled(t, "RecordMiss", "test_cache")
cacheMock.AssertCalled(t, "Get", "key")
collectorMock.AssertExpectations(t)
cacheMock.AssertExpectations(t)
}
func TestCacheClear(t *testing.T) {
c, cacheMock, collectorMock := setupCache[string](t)
collectorMock.On("RecordSet", "test_cache").Twice()
cacheMock.On("Set", "key1", "value1").Once()
cacheMock.On("Set", "key2", "value2").Once()
collectorMock.On("RecordClear", "test_cache").Once()
cacheMock.On("Clear").Once()
cacheMock.On("Get", "key1").Return("", false).Once()
cacheMock.On("Get", "key2").Return("", false).Once()
c.Set("key1", "value1")
c.Set("key2", "value2")
collectorMock.AssertNumberOfCalls(t, "RecordSet", 2)
cacheMock.AssertCalled(t, "Set", "key1", "value1")
cacheMock.AssertCalled(t, "Set", "key2", "value2")
c.Clear()
collectorMock.AssertCalled(t, "RecordClear", "test_cache")
cacheMock.AssertCalled(t, "Clear")
collectorMock.On("RecordMiss", "test_cache").Twice()
_, found1 := c.Get("key1")
_, found2 := c.Get("key2")
assert.False(t, found1, "Expected not to find key1 after clear")
assert.False(t, found2, "Expected not to find key2 after clear")
collectorMock.AssertNumberOfCalls(t, "RecordMiss", 2)
cacheMock.AssertCalled(t, "Get", "key1")
cacheMock.AssertCalled(t, "Get", "key2")
collectorMock.AssertExpectations(t)
cacheMock.AssertExpectations(t)
}
func TestCacheCount(t *testing.T) {
c, cacheMock, collectorMock := setupCache[string](t)
collectorMock.On("RecordSet", "test_cache").Times(3)
cacheMock.On("Set", mock.Anything, mock.Anything).Times(3)
cacheMock.On("Count").Return(3).Once()
collectorMock.On("RecordDelete", "test_cache").Once()
cacheMock.On("Delete", "key2").Once()
cacheMock.On("Count").Return(2).Once()
collectorMock.On("RecordClear", "test_cache").Once()
cacheMock.On("Clear").Once()
cacheMock.On("Count").Return(0).Once()
c.Set("key1", "value1")
c.Set("key2", "value2")
c.Set("key3", "value3")
assert.Equal(t, 3, c.Count(), "Expected count to be 3")
collectorMock.AssertNumberOfCalls(t, "RecordSet", 3)
cacheMock.AssertNumberOfCalls(t, "Set", 3)
cacheMock.AssertCalled(t, "Count")
c.Delete("key2")
assert.Equal(t, 2, c.Count(), "Expected count to be 2 after deletion")
collectorMock.AssertCalled(t, "RecordDelete", "test_cache")
cacheMock.AssertCalled(t, "Delete", "key2")
cacheMock.AssertCalled(t, "Count")
c.Clear()
assert.Equal(t, 0, c.Count(), "Expected count to be 0 after clear")
collectorMock.AssertCalled(t, "RecordClear", "test_cache")
cacheMock.AssertCalled(t, "Clear")
cacheMock.AssertCalled(t, "Count")
collectorMock.AssertExpectations(t)
cacheMock.AssertExpectations(t)
}
func TestCacheKeys(t *testing.T) {
c, cacheMock, collectorMock := setupCache[string](t)
collectorMock.On("RecordSet", "test_cache").Times(3)
cacheMock.On("Set", mock.Anything, mock.Anything).Times(3)
collectorMock.On("RecordDelete", "test_cache").Once()
cacheMock.On("Delete", "key2").Once()
cacheMock.On("Clear").Once()
collectorMock.On("RecordClear", "test_cache").Once()
cacheMock.On("Keys").Return([]string{"key1", "key2", "key3"}).Once()
cacheMock.On("Keys").Return([]string{"key1", "key3"}).Once()
cacheMock.On("Keys").Return([]string{}).Once()
c.Set("key1", "value1")
c.Set("key2", "value2")
c.Set("key3", "value3")
collectorMock.AssertNumberOfCalls(t, "RecordSet", 3)
cacheMock.AssertNumberOfCalls(t, "Set", 3)
keys := c.Keys()
assert.Len(t, keys, 3, "Expected 3 keys")
assert.ElementsMatch(t, []string{"key1", "key2", "key3"}, keys, "Keys do not match expected values")
c.Delete("key2")
keys = c.Keys()
assert.Len(t, keys, 2, "Expected 2 keys after deletion")
assert.ElementsMatch(t, []string{"key1", "key3"}, keys, "Keys do not match expected values after deletion")
collectorMock.AssertCalled(t, "RecordDelete", "test_cache")
c.Clear()
keys = c.Keys()
assert.Len(t, keys, 0, "Expected no keys after clear")
collectorMock.AssertCalled(t, "RecordClear", "test_cache")
collectorMock.AssertExpectations(t)
cacheMock.AssertExpectations(t)
}
func TestCacheValues(t *testing.T) {
c, cacheMock, collectorMock := setupCache[string](t)
collectorMock.On("RecordSet", "test_cache").Times(3)
cacheMock.On("Set", mock.Anything, mock.Anything).Times(3)
collectorMock.On("RecordDelete", "test_cache").Once()
cacheMock.On("Delete", "key2").Once()
collectorMock.On("RecordClear", "test_cache").Once()
cacheMock.On("Clear").Once()
cacheMock.On("Values").Return([]string{"value1", "value2", "value3"}).Once()
cacheMock.On("Values").Return([]string{"value1", "value3"}).Once()
cacheMock.On("Values").Return([]string{}).Once()
c.Set("key1", "value1")
c.Set("key2", "value2")
c.Set("key3", "value3")
collectorMock.AssertNumberOfCalls(t, "RecordSet", 3)
cacheMock.AssertNumberOfCalls(t, "Set", 3)
values := c.Values()
assert.Len(t, values, 3, "Expected 3 values")
assert.ElementsMatch(t, []string{"value1", "value2", "value3"}, values, "Values do not match expected values")
c.Delete("key2")
values = c.Values()
assert.Len(t, values, 2, "Expected 2 values after deletion")
assert.ElementsMatch(t, []string{"value1", "value3"}, values, "Values do not match expected values after deletion")
collectorMock.AssertCalled(t, "RecordDelete", "test_cache")
c.Clear()
values = c.Values()
assert.Len(t, values, 0, "Expected no values after clear")
collectorMock.AssertCalled(t, "RecordClear", "test_cache")
collectorMock.AssertExpectations(t)
cacheMock.AssertExpectations(t)
}
func TestCacheContents(t *testing.T) {
c, cacheMock, collectorMock := setupCache[string](t)
collectorMock.On("RecordSet", "test_cache").Times(3)
cacheMock.On("Set", mock.Anything, mock.Anything).Times(3)
collectorMock.On("RecordDelete", "test_cache").Once()
cacheMock.On("Delete", "key2").Once()
collectorMock.On("RecordClear", "test_cache").Once()
cacheMock.On("Clear").Once()
cacheMock.On("Contents").Return("key1, key2, key3").Once()
cacheMock.On("Contents").Return("key1, key3").Once()
cacheMock.On("Contents").Return("[]").Once()
c.Set("key1", "value1")
c.Set("key2", "value2")
c.Set("key3", "value3")
collectorMock.AssertNumberOfCalls(t, "RecordSet", 3)
cacheMock.AssertNumberOfCalls(t, "Set", 3)
contents := c.Contents()
assert.Contains(t, contents, "key1", "Contents should contain key1")
assert.Contains(t, contents, "key2", "Contents should contain key2")
assert.Contains(t, contents, "key3", "Contents should contain key3")
c.Delete("key2")
contents = c.Contents()
assert.Contains(t, contents, "key1", "Contents should contain key1")
assert.NotContains(t, contents, "key2", "Contents should not contain key2")
assert.Contains(t, contents, "key3", "Contents should contain key3")
collectorMock.AssertCalled(t, "RecordDelete", "test_cache")
c.Clear()
contents = c.Contents()
assert.Equal(t, "[]", contents, "Contents should be empty after clear")
collectorMock.AssertCalled(t, "RecordClear", "test_cache")
collectorMock.AssertExpectations(t)
cacheMock.AssertExpectations(t)
}
================================================
FILE: pkg/cache/lru/lru.go
================================================
// Package lru provides a generic, size-limited, LRU (Least Recently Used) cache with optional
// metrics collection and reporting. It wraps the golang-lru/v2 caching library, adding support for custom
// metrics tracking cache hits, misses, evictions, and other cache operations.
//
// This package supports configuring key aspects of cache behavior, including maximum cache size,
// and custom metrics collection.
package lru
import (
"fmt"
lru "github.com/hashicorp/golang-lru/v2"
"github.com/trufflesecurity/trufflehog/v3/pkg/cache"
)
// Cache is a generic LRU-sized cache that stores key-value pairs with a maximum size limit.
// It wraps the lru.Cache library and adds support for custom metrics collection.
type Cache[T any] struct {
cache *lru.Cache[string, T]
cacheName string
capacity int
evictMetrics cache.EvictionMetricsCollector
}
// Option defines a functional option for configuring the Cache.
type Option[T any] func(*Cache[T])
// WithCapacity is a functional option to set the maximum number of items the cache can hold.
// If the capacity is not set, the default value (128_000) is used.
func WithCapacity[T any](capacity int) Option[T] {
return func(lc *Cache[T]) { lc.capacity = capacity }
}
// WithMetricsCollector is a functional option to set a custom metrics collector.
func WithMetricsCollector[T any](collector cache.EvictionMetricsCollector) Option[T] {
return func(lc *Cache[T]) { lc.evictMetrics = collector }
}
// NewCache creates a new Cache with optional configuration parameters.
// It takes a cache name and a variadic list of options.
func NewCache[T any](cacheName string, opts ...Option[T]) (*Cache[T], error) {
// Default values for cache configuration.
const defaultSize = 128_000
sizedLRU := &Cache[T]{
cacheName: cacheName,
}
for _, opt := range opts {
opt(sizedLRU)
}
var onEvicted func(string, T)
// Provide a evict callback function to record evictions if a custom metrics collector is provided.
if sizedLRU.evictMetrics != nil {
onEvicted = func(string, T) {
sizedLRU.evictMetrics.RecordEviction(sizedLRU.cacheName)
}
}
lcache, err := lru.NewWithEvict[string, T](defaultSize, onEvicted)
if err != nil {
return nil, fmt.Errorf("failed to create lrusized cache: %w", err)
}
sizedLRU.cache = lcache
return sizedLRU, nil
}
// Set adds a key-value pair to the cache.
func (lc *Cache[T]) Set(key string, val T) { lc.cache.Add(key, val) }
// Get retrieves a value from the cache by key.
func (lc *Cache[T]) Get(key string) (T, bool) {
value, found := lc.cache.Get(key)
if found {
return value, true
}
var zero T
return zero, false
}
// Exists checks if a key exists in the cache.
func (lc *Cache[T]) Exists(key string) bool {
_, found := lc.cache.Get(key)
return found
}
// Delete removes a key from the cache.
func (lc *Cache[T]) Delete(key string) {
lc.cache.Remove(key)
}
// Clear removes all keys from the cache.
func (lc *Cache[T]) Clear() {
lc.cache.Purge()
}
// Count returns the number of key-value pairs in the cache.
func (lc *Cache[T]) Count() int { return lc.cache.Len() }
// Keys returns all keys in the cache.
func (lc *Cache[T]) Keys() []string { return lc.cache.Keys() }
// Values returns all values in the cache.
func (lc *Cache[T]) Values() []T {
items := lc.cache.Keys()
res := make([]T, 0, len(items))
for _, k := range items {
v, _ := lc.cache.Get(k)
res = append(res, v)
}
return res
}
// Contents returns all keys in the cache encoded as a string.
func (lc *Cache[T]) Contents() string {
return fmt.Sprintf("%v", lc.cache.Keys())
}
================================================
FILE: pkg/cache/lru/lru_test.go
================================================
package lru
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type mockCollector struct{ mock.Mock }
func (m *mockCollector) RecordEviction(cacheName string) { m.Called(cacheName) }
// setupCache initializes the metrics and cache.
// If withCollector is true, it sets up a cache with a custom metrics collector.
// Otherwise, it sets up a cache without a custom metrics collector.
func setupCache[T any](t *testing.T, withCollector bool) (*Cache[T], *mockCollector) {
t.Helper()
var collector *mockCollector
var c *Cache[T]
var err error
if withCollector {
collector = new(mockCollector)
c, err = NewCache[T]("test_cache", WithMetricsCollector[T](collector))
} else {
c, err = NewCache[T]("test_cache")
}
assert.NoError(t, err, "Failed to create cache")
assert.NotNil(t, c, "Cache should not be nil")
return c, collector
}
func TestNewLRUCache(t *testing.T) {
t.Run("default configuration", func(t *testing.T) {
c, _ := setupCache[int](t, false)
assert.Equal(t, "test_cache", c.cacheName)
})
t.Run("with custom max cost", func(t *testing.T) {
c, _ := setupCache[int](t, false)
assert.NotNil(t, c)
})
t.Run("with metrics collector", func(t *testing.T) {
c, collector := setupCache[int](t, true)
assert.NotNil(t, c)
assert.Equal(t, "test_cache", c.cacheName)
assert.Equal(t, collector, c.evictMetrics, "Cache metrics should match the collector")
})
}
func TestCacheSet(t *testing.T) {
c, _ := setupCache[string](t, true)
c.Set("key", "value")
value, found := c.Get("key")
assert.True(t, found, "Expected to find the key")
assert.Equal(t, "value", value, "Expected value to match")
}
func TestCacheGet(t *testing.T) {
c, _ := setupCache[string](t, true)
c.Set("key", "value")
value, found := c.Get("key")
assert.True(t, found, "Expected to find the key")
assert.Equal(t, "value", value, "Expected value to match")
_, found = c.Get("non_existent")
assert.False(t, found, "Expected not to find the key")
}
func TestCacheExists(t *testing.T) {
c, _ := setupCache[string](t, true)
c.Set("key", "value")
exists := c.Exists("key")
assert.True(t, exists, "Expected the key to exist")
exists = c.Exists("non_existent")
assert.False(t, exists, "Expected the key not to exist")
}
func TestCacheDelete(t *testing.T) {
c, collector := setupCache[string](t, true)
collector.On("RecordEviction", "test_cache").Once()
c.Set("key", "value")
c.Delete("key")
collector.AssertCalled(t, "RecordEviction", "test_cache")
_, found := c.Get("key")
assert.False(t, found, "Expected not to find the deleted key")
}
func TestCacheClear(t *testing.T) {
c, collector := setupCache[string](t, true)
collector.On("RecordEviction", "test_cache").Twice()
c.Set("key1", "value1")
c.Set("key2", "value2")
c.Clear()
collector.AssertNumberOfCalls(t, "RecordEviction", 2)
_, found1 := c.Get("key1")
_, found2 := c.Get("key2")
assert.False(t, found1, "Expected not to find key1 after clear")
assert.False(t, found2, "Expected not to find key2 after clear")
}
func TestCacheCount(t *testing.T) {
c, collector := setupCache[string](t, true)
collector.On("RecordEviction", "test_cache").Times(3)
c.Set("key1", "value1")
c.Set("key2", "value2")
c.Set("key3", "value3")
assert.Equal(t, 3, c.Count(), "Expected count to be 3")
c.Delete("key2")
assert.Equal(t, 2, c.Count(), "Expected count to be 2 after deletion")
collector.AssertNumberOfCalls(t, "RecordEviction", 1)
c.Clear()
assert.Equal(t, 0, c.Count(), "Expected count to be 0 after clear")
collector.AssertNumberOfCalls(t, "RecordEviction", 3)
}
func TestCacheKeys(t *testing.T) {
c, collector := setupCache[string](t, true)
collector.On("RecordEviction", "test_cache").Times(3)
c.Set("key1", "value1")
c.Set("key2", "value2")
c.Set("key3", "value3")
keys := c.Keys()
assert.Len(t, keys, 3, "Expected 3 keys")
assert.ElementsMatch(t, []string{"key1", "key2", "key3"}, keys, "Keys do not match expected values")
c.Delete("key2")
keys = c.Keys()
assert.Len(t, keys, 2, "Expected 2 keys after deletion")
assert.ElementsMatch(t, []string{"key1", "key3"}, keys, "Keys do not match expected values after deletion")
collector.AssertNumberOfCalls(t, "RecordEviction", 1)
c.Clear()
keys = c.Keys()
assert.Len(t, keys, 0, "Expected no keys after clear")
collector.AssertNumberOfCalls(t, "RecordEviction", 3)
}
func TestCacheValues(t *testing.T) {
c, collector := setupCache[string](t, true)
collector.On("RecordEviction", "test_cache").Times(3)
c.Set("key1", "value1")
c.Set("key2", "value2")
c.Set("key3", "value3")
values := c.Values()
assert.Len(t, values, 3, "Expected 3 values")
assert.ElementsMatch(t, []string{"value1", "value2", "value3"}, values, "Values do not match expected values")
c.Delete("key2")
values = c.Values()
assert.Len(t, values, 2, "Expected 2 values after deletion")
assert.ElementsMatch(t, []string{"value1", "value3"}, values, "Values do not match expected values after deletion")
collector.AssertNumberOfCalls(t, "RecordEviction", 1)
c.Clear()
values = c.Values()
assert.Len(t, values, 0, "Expected no values after clear")
collector.AssertNumberOfCalls(t, "RecordEviction", 3)
}
func TestCacheContents(t *testing.T) {
c, collector := setupCache[string](t, true)
collector.On("RecordEviction", "test_cache").Times(3)
c.Set("key1", "value1")
c.Set("key2", "value2")
c.Set("key3", "value3")
contents := c.Contents()
assert.Contains(t, contents, "key1", "Contents should contain key1")
assert.Contains(t, contents, "key2", "Contents should contain key2")
assert.Contains(t, contents, "key3", "Contents should contain key3")
c.Delete("key2")
contents = c.Contents()
assert.Contains(t, contents, "key1", "Contents should contain key1")
assert.NotContains(t, contents, "key2", "Contents should not contain key2")
assert.Contains(t, contents, "key3", "Contents should contain key3")
collector.AssertNumberOfCalls(t, "RecordEviction", 1)
c.Clear()
contents = c.Contents()
assert.Equal(t, "[]", contents, "Contents should be empty after clear")
collector.AssertNumberOfCalls(t, "RecordEviction", 3)
}
================================================
FILE: pkg/cache/metrics.go
================================================
package cache
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
)
// BaseMetricsCollector defines the interface for recording cache metrics.
// Each method corresponds to a specific cache-related operation.
type BaseMetricsCollector interface {
RecordHit(cacheName string)
RecordMiss(cacheName string)
RecordSet(cacheName string)
RecordDelete(cacheName string)
RecordClear(cacheName string)
}
// EvictionMetricsCollector defines the interface for recording cache-specific eviction metrics.
type EvictionMetricsCollector interface {
RecordEviction(cacheName string)
}
// baseCollector encapsulates all Prometheus metrics with labels.
// It holds Prometheus counters for cache operations, which help track
// the performance and usage of the cache.
type baseCollector struct {
// Base metrics.
hits *prometheus.CounterVec
misses *prometheus.CounterVec
sets *prometheus.CounterVec
deletes *prometheus.CounterVec
clears *prometheus.CounterVec
}
func init() {
// Initialize the singleton baseCollector.
// Set up Prometheus counters for cache operations (hits, misses, sets, deletes, clears).
baseMetricsInstance = &baseCollector{
hits: promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "hits_total",
Help: "Total number of cache hits.",
}, []string{"cache_name"}),
misses: promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "misses_total",
Help: "Total number of cache misses.",
}, []string{"cache_name"}),
sets: promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "sets_total",
Help: "Total number of cache set operations.",
}, []string{"cache_name"}),
deletes: promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "deletes_total",
Help: "Total number of cache delete operations.",
}, []string{"cache_name"}),
clears: promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "clears_total",
Help: "Total number of cache clear operations.",
}, []string{"cache_name"}),
}
// Initialize the singleton evictionMetrics.
// Set up Prometheus counters for cache evictions.
evictionMetricsInstance = &evictionMetrics{
evictions: promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "evictions_total",
Help: "Total number of cache evictions.",
}, []string{"cache_name"}),
}
}
var (
baseMetricsInstance *baseCollector
evictionMetricsInstance *evictionMetrics
)
// GetBaseMetricsCollector returns the singleton baseCollector instance.
func GetBaseMetricsCollector() BaseMetricsCollector { return baseMetricsInstance }
// GetEvictionMetricsCollector returns the singleton evictionMetrics instance.
func GetEvictionMetricsCollector() EvictionMetricsCollector { return evictionMetricsInstance }
// Implement BaseMetricsCollector interface methods.
// RecordHit increments the counter for cache hits, tracking how often cache lookups succeed.
func (m *baseCollector) RecordHit(cacheName string) { m.hits.WithLabelValues(cacheName).Inc() }
// RecordMiss increments the counter for cache misses, tracking how often cache lookups fail.
func (m *baseCollector) RecordMiss(cacheName string) { m.misses.WithLabelValues(cacheName).Inc() }
// RecordSet increments the counter for cache set operations, tracking how often items are added/updated.
func (m *baseCollector) RecordSet(cacheName string) { m.sets.WithLabelValues(cacheName).Inc() }
// RecordDelete increments the counter for cache delete operations, tracking how often items are removed.
func (m *baseCollector) RecordDelete(cacheName string) { m.deletes.WithLabelValues(cacheName).Inc() }
// RecordClear increments the counter for cache clear operations, tracking how often the cache is completely cleared.
func (m *baseCollector) RecordClear(cacheName string) { m.clears.WithLabelValues(cacheName).Inc() }
// evictionMetrics implements EvictionMetricsCollector interface.
type evictionMetrics struct {
evictions *prometheus.CounterVec
}
// Implement EvictionMetricsCollector interface method.
func (em *evictionMetrics) RecordEviction(cacheName string) {
em.evictions.WithLabelValues(cacheName).Inc()
}
================================================
FILE: pkg/cache/simple/simple.go
================================================
package simple
import (
"strings"
"time"
"github.com/patrickmn/go-cache"
)
const (
defaultExpirationInterval = 12 * time.Hour
defaultPurgeInterval = 13 * time.Hour
defaultExpiration = cache.DefaultExpiration
)
// Cache wraps the go-cache library to provide an in-memory key-value store.
type Cache[T any] struct {
c *cache.Cache
expiration time.Duration
purgeInterval time.Duration
}
// CacheOption defines a function type used for configuring a Cache.
type CacheOption[T any] func(*Cache[T])
// WithExpirationInterval returns a CacheOption to set the expiration interval of cache items.
// The interval determines the duration a cached item remains in the cache before it is expired.
func WithExpirationInterval[T any](interval time.Duration) CacheOption[T] {
return func(c *Cache[T]) { c.expiration = interval }
}
// WithPurgeInterval returns a CacheOption to set the interval at which the cache purges expired items.
// Regular purging helps in freeing up memory by removing stale entries.
func WithPurgeInterval[T any](interval time.Duration) CacheOption[T] {
return func(c *Cache[T]) { c.purgeInterval = interval }
}
// NewCache constructs a new in-memory cache instance with optional configurations.
// By default, it sets the expiration and purge intervals to 12 and 13 hours, respectively.
// These defaults can be overridden using the functional options: WithExpirationInterval and WithPurgeInterval.
func NewCache[T any](opts ...CacheOption[T]) *Cache[T] {
return NewCacheWithData[T](nil, opts...)
}
// CacheEntry represents a single entry in the cache, consisting of a key and its corresponding value.
type CacheEntry[T any] struct {
// Key is the unique identifier for the entry.
Key string
// Value is the data stored in the entry.
Value T
}
// NewCacheWithData constructs a new in-memory cache with existing data.
// It also accepts CacheOption parameters to override default configuration values.
func NewCacheWithData[T any](data []CacheEntry[T], opts ...CacheOption[T]) *Cache[T] {
instance := &Cache[T]{expiration: defaultExpirationInterval, purgeInterval: defaultPurgeInterval}
for _, opt := range opts {
opt(instance)
}
// Convert data slice to map required by go-cache.
items := make(map[string]cache.Item, len(data))
for _, d := range data {
items[d.Key] = cache.Item{Object: d.Value, Expiration: int64(defaultExpiration)}
}
instance.c = cache.NewFrom(instance.expiration, instance.purgeInterval, items)
return instance
}
// Set adds a key-value pair to the cache.
func (c *Cache[T]) Set(key string, value T) {
c.c.Set(key, value, defaultExpiration)
}
// Get returns the value for the given key.
func (c *Cache[T]) Get(key string) (T, bool) {
var value T
v, ok := c.c.Get(key)
if !ok {
return value, false
}
value, ok = v.(T)
return value, ok
}
// Exists returns true if the given key exists in the cache.
func (c *Cache[T]) Exists(key string) bool {
_, ok := c.c.Get(key)
return ok
}
// Delete removes the key-value pair from the cache.
func (c *Cache[T]) Delete(key string) {
c.c.Delete(key)
}
// Clear removes all key-value pairs from the cache.
func (c *Cache[T]) Clear() {
c.c.Flush()
}
// Count returns the number of key-value pairs in the cache.
func (c *Cache[T]) Count() int {
return c.c.ItemCount()
}
// Keys returns all keys in the cache.
func (c *Cache[T]) Keys() []string {
items := c.c.Items()
res := make([]string, 0, len(items))
for k := range items {
res = append(res, k)
}
return res
}
// Values returns all values in the cache.
func (c *Cache[T]) Values() []T {
items := c.c.Items()
res := make([]T, 0, len(items))
for _, v := range items {
obj, ok := v.Object.(T)
if ok {
res = append(res, obj)
}
}
return res
}
// Contents returns a comma-separated string containing all keys in the cache.
func (c *Cache[T]) Contents() string {
items := c.c.Items()
res := make([]string, 0, len(items))
for k := range items {
res = append(res, k)
}
return strings.Join(res, ",")
}
================================================
FILE: pkg/cache/simple/simple_test.go
================================================
package simple
import (
"fmt"
"sort"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestCache(t *testing.T) {
c := NewCache[string]()
// Test set and get.
c.Set("key1", "key1")
v, ok := c.Get("key1")
if !ok || v != "key1" {
t.Fatalf("Unexpected value for key1: %v, %v", v, ok)
}
// Test exists.
if !c.Exists("key1") {
t.Fatalf("Expected key1 to exist")
}
// Test the count.
if c.Count() != 1 {
t.Fatalf("Unexpected count: %d", c.Count())
}
// Test delete.
c.Delete("key1")
v, ok = c.Get("key1")
if ok || v != "" {
t.Fatalf("Unexpected value for key1 after delete: %v, %v", v, ok)
}
// Test clear.
c.Set("key10", "key10")
c.Clear()
v, ok = c.Get("key10")
if ok || v != "" {
t.Fatalf("Unexpected value for key10 after clear: %v, %v", v, ok)
}
// Test getting only the keys.
keys := []string{"key1", "key2", "key3"}
values := []string{"value1", "value2", "value3"}
for i, k := range keys {
c.Set(k, values[i])
}
k := c.Keys()
sort.Strings(keys)
sort.Strings(k)
if !cmp.Equal(keys, k) {
t.Fatalf("Unexpected keys: %v", k)
}
// Test getting only the values.
vals := make([]string, 0, c.Count())
vals = append(vals, c.Values()...)
sort.Strings(vals)
sort.Strings(values)
if !cmp.Equal(values, vals) {
t.Fatalf("Unexpected values: %v", vals)
}
// Test contents.
items := c.Contents()
sort.Strings(keys)
res := strings.Split(items, ",")
sort.Strings(res)
if len(keys) != len(res) {
t.Fatalf("Unexpected length of items: %d", len(res))
}
if !cmp.Equal(keys, res) {
t.Fatalf("Unexpected items: %v", res)
}
}
func TestCache_NewWithData(t *testing.T) {
data := []CacheEntry[string]{{"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}}
c := NewCacheWithData(data)
// Test the count.
if c.Count() != 3 {
t.Fatalf("Unexpected count: %d", c.Count())
}
// Test contents.
keys := []string{"key1", "key2", "key3"}
items := c.Contents()
sort.Strings(keys)
res := strings.Split(items, ",")
sort.Strings(res)
if len(keys) != len(res) {
t.Fatalf("Unexpected length of items: %d", len(res))
}
if !cmp.Equal(keys, res) {
t.Fatalf("Unexpected items: %v", res)
}
}
func setupBenchmarks(b *testing.B) *Cache[string] {
b.Helper()
c := NewCache[string]()
for i := 0; i < 500_000; i++ {
key := fmt.Sprintf("key%d", i)
c.Set(key, key)
}
return c
}
func BenchmarkSet(b *testing.B) {
c := NewCache[string]()
for i := 0; i < b.N; i++ {
key := fmt.Sprintf("key%d", i)
c.Set(key, key)
}
}
func BenchmarkGet(b *testing.B) {
c := setupBenchmarks(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
key := fmt.Sprintf("key%d", i)
c.Get(key)
}
}
func BenchmarkDelete(b *testing.B) {
c := setupBenchmarks(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
key := fmt.Sprintf("key%d", i)
c.Delete(key)
}
}
func BenchmarkCount(b *testing.B) {
c := setupBenchmarks(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Count()
}
}
func BenchmarkContents(b *testing.B) {
c := setupBenchmarks(b)
b.ResetTimer()
var s string
for i := 0; i < b.N; i++ {
s = c.Contents()
}
_ = s
}
================================================
FILE: pkg/channelmetrics/metrics_collector/prometheus/collector.go
================================================
package prometheus
import (
"fmt"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
// MetricsCollector implements the |channelmetrics.MetricsCollector| interface using Prometheus.
// It records various metrics related to channel operations.
type MetricsCollector struct {
produceDuration prometheus.Histogram
consumeDuration prometheus.Histogram
channelLen prometheus.Gauge
channelCap prometheus.Gauge
}
var (
collectors = make(map[string]*MetricsCollector)
collectorsMu sync.Mutex
)
// NewMetricsCollector creates a new MetricsCollector with
// histograms for produce and consume durations, and gauges for channel length and capacity.
// It accepts namespace, subsystem, and chanName parameters to organize metrics.
// The function initializes and returns a pointer to a MetricsCollector struct
// that contains the following Prometheus metrics:
//
// - produceDuration: a Histogram metric that measures the duration of producing an item.
// It tracks the time taken to add an item to the ObservableChan.
// This metric helps to monitor the performance and latency of item production.
//
// - consumeDuration: a Histogram metric that measures the duration of consuming an item.
// It tracks the time taken to retrieve an item from the ObservableChan.
// This metric helps to monitor the performance and latency of item consumption.
//
// - channelLen: a Gauge metric that measures the current size of the channel buffer.
// It tracks the number of items in the channel buffer at any given time.
// This metric helps to monitor the utilization of the channel buffer.
//
// - channelCap: a Gauge metric that measures the capacity of the channel buffer.
// It tracks the maximum number of items that the channel buffer can hold.
// This metric helps to understand the configuration and potential limits of the channel buffer.
//
// These metrics are useful for monitoring the performance and throughput of the ObservableChan.
// By tracking the durations of item production and consumption, as well as the buffer size and capacity,
// you can identify bottlenecks, optimize performance, and ensure that the ObservableChan is operating efficiently.
func NewMetricsCollector(chanName, namespace, subsystem string) *MetricsCollector {
key := fmt.Sprintf("%s_%s_%s", namespace, subsystem, chanName)
collectorsMu.Lock()
defer collectorsMu.Unlock()
if collector, exists := collectors[key]; exists {
return collector
}
collector := &MetricsCollector{
produceDuration: promauto.NewHistogram(prometheus.HistogramOpts{
Name: metricName(chanName, "produce_duration_microseconds"),
Namespace: namespace,
Subsystem: subsystem,
Help: "Duration of producing an item in microseconds.",
Buckets: prometheus.ExponentialBuckets(1, 2, 20),
}),
consumeDuration: promauto.NewHistogram(prometheus.HistogramOpts{
Name: metricName(chanName, "consume_duration_microseconds"),
Namespace: namespace,
Subsystem: subsystem,
Help: "Duration of consuming an item in microseconds.",
Buckets: prometheus.ExponentialBuckets(1, 2, 20),
}),
channelLen: promauto.NewGauge(prometheus.GaugeOpts{
Name: metricName(chanName, "channel_length"),
Namespace: namespace,
Subsystem: subsystem,
Help: "Current size of the channel buffer.",
}),
channelCap: promauto.NewGauge(prometheus.GaugeOpts{
Name: metricName(chanName, "channel_capacity"),
Namespace: namespace,
Subsystem: subsystem,
Help: "Capacity of the channel buffer.",
}),
}
collectors[key] = collector
return collector
}
// metricName constructs a full metric name by combining the channel name with the specific metric.
func metricName(chanName, metric string) string { return chanName + "_" + metric }
// RecordProduceDuration records the duration taken to produce an item into the channel.
func (c *MetricsCollector) RecordProduceDuration(duration time.Duration) {
c.produceDuration.Observe(float64(duration.Microseconds()))
}
// RecordConsumeDuration records the duration taken to consume an item from the channel.
func (c *MetricsCollector) RecordConsumeDuration(duration time.Duration) {
c.consumeDuration.Observe(float64(duration.Microseconds()))
}
// RecordChannelLen records the current size of the channel buffer.
func (c *MetricsCollector) RecordChannelLen(size int) { c.channelLen.Set(float64(size)) }
// RecordChannelCap records the capacity of the channel buffer.
func (c *MetricsCollector) RecordChannelCap(capacity int) { c.channelCap.Set(float64(capacity)) }
================================================
FILE: pkg/channelmetrics/noopcollector.go
================================================
package channelmetrics
import "time"
// noopCollector is a default implementation of the MetricsCollector interface
// for internal package use only.
type noopCollector struct{}
func (noopCollector) RecordProduceDuration(duration time.Duration) {}
func (noopCollector) RecordConsumeDuration(duration time.Duration) {}
func (noopCollector) RecordChannelLen(size int) {}
func (noopCollector) RecordChannelCap(capacity int) {}
================================================
FILE: pkg/channelmetrics/observablechan.go
================================================
// Package channelmetrics provides a flexible way to wrap Go channels with
// additional metrics collection capabilities. This allows for monitoring
// and tracking of channel usage and performance using different metrics backends.
package channelmetrics
import (
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
// MetricsCollector is an interface for collecting metrics. Implementations
// of this interface can be used to record various channel metrics.
type MetricsCollector interface {
RecordProduceDuration(duration time.Duration)
RecordConsumeDuration(duration time.Duration)
RecordChannelLen(size int)
RecordChannelCap(capacity int)
}
// ObservableChan wraps a Go channel and collects metrics about its usage.
// It supports any type of channel and records metrics using a provided
// MetricsCollector implementation.
type ObservableChan[T any] struct {
ch chan T
metrics MetricsCollector
}
// NewObservableChan creates a new ObservableChan wrapping the provided channel.
// It records the channel's capacity immediately and sets up metrics collection
// using the provided MetricsCollector and channel name. The chanName is used to
// distinguish between metrics for different channels by incorporating it into
// the metric names.
func NewObservableChan[T any](ch chan T, metrics MetricsCollector) *ObservableChan[T] {
if metrics == nil {
metrics = noopCollector{}
}
oChan := &ObservableChan[T]{
ch: ch,
metrics: metrics,
}
oChan.RecordChannelCapacity()
// Record the current length of the channel.
// Note: The channel is likely empty, but it may contain items if it
// was pre-existing.
oChan.RecordChannelLen()
return oChan
}
// Close closes the channel and records the current size of the channel buffer.
func (oc *ObservableChan[T]) Close() {
close(oc.ch)
oc.RecordChannelLen()
}
// Send sends an item into the channel and records the duration taken to do so.
// It also updates the current size of the channel buffer. This method blocks
// until the item is sent.
func (oc *ObservableChan[T]) Send(item T) { _ = oc.SendCtx(context.Background(), item) }
// SendCtx sends an item into the channel with context and records the duration
// taken to do so. It also updates the current size of the channel buffer and
// supports context cancellation.
func (oc *ObservableChan[T]) SendCtx(ctx context.Context, item T) error {
defer func(start time.Time) {
oc.metrics.RecordProduceDuration(time.Since(start))
oc.RecordChannelLen()
}(time.Now())
return common.CancellableWrite(ctx, oc.ch, item)
}
// Recv receives an item from the channel and records the duration taken to do
// so. It also updates the current size of the channel buffer. This method
// blocks until an item is available.
func (oc *ObservableChan[T]) Recv() T {
v, _ := oc.RecvCtx(context.Background())
return v
}
// RecvCtx receives an item from the channel with context and records the
// duration taken to do so. It also updates the current size of the channel
// buffer and supports context cancellation. If an error occurs, it logs the
// error.
func (oc *ObservableChan[T]) RecvCtx(ctx context.Context) (T, error) {
defer func(start time.Time) {
oc.metrics.RecordConsumeDuration(time.Since(start))
oc.RecordChannelLen()
}(time.Now())
return common.CancellableRead(ctx, oc.ch)
}
// RecordChannelCapacity records the capacity of the channel buffer.
func (oc *ObservableChan[T]) RecordChannelCapacity() { oc.metrics.RecordChannelCap(cap(oc.ch)) }
// RecordChannelLen records the current size of the channel buffer.
func (oc *ObservableChan[T]) RecordChannelLen() { oc.metrics.RecordChannelLen(len(oc.ch)) }
================================================
FILE: pkg/channelmetrics/observablechan_test.go
================================================
package channelmetrics
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
type MockMetricsCollector struct{ mock.Mock }
func (m *MockMetricsCollector) RecordProduceDuration(duration time.Duration) { m.Called(duration) }
func (m *MockMetricsCollector) RecordConsumeDuration(duration time.Duration) { m.Called(duration) }
func (m *MockMetricsCollector) RecordChannelLen(size int) { m.Called(size) }
func (m *MockMetricsCollector) RecordChannelCap(capacity int) { m.Called(capacity) }
func TestObservableChanSend(t *testing.T) {
t.Parallel()
mockMetrics := new(MockMetricsCollector)
bufferCap := 10
mockMetrics.On("RecordProduceDuration", mock.Anything).Once()
mockMetrics.On("RecordChannelLen", mock.AnythingOfType("int")).Twice()
mockMetrics.On("RecordChannelCap", bufferCap).Once()
ch := make(chan int, bufferCap)
oc := NewObservableChan(ch, mockMetrics)
assert.Equal(t, bufferCap, cap(oc.ch))
err := oc.SendCtx(context.Background(), 1)
assert.NoError(t, err)
mockMetrics.AssertExpectations(t)
}
func TestObservableChanRecv(t *testing.T) {
t.Parallel()
mockMetrics := new(MockMetricsCollector)
bufferCap := 10
mockMetrics.On("RecordConsumeDuration", mock.Anything).Once() // For the send
mockMetrics.On("RecordProduceDuration", mock.Anything).Once()
mockMetrics.On("RecordChannelLen", mock.AnythingOfType("int")).Times(3) // For the send and recv
mockMetrics.On("RecordChannelCap", bufferCap).Once()
ch := make(chan int, bufferCap)
oc := NewObservableChan(ch, mockMetrics)
assert.Equal(t, bufferCap, cap(oc.ch))
go func() {
err := oc.SendCtx(context.Background(), 1)
assert.NoError(t, err)
}()
time.Sleep(100 * time.Millisecond) // Ensure Send happens before Recv
_, err := oc.RecvCtx(context.Background())
assert.NoError(t, err)
mockMetrics.AssertExpectations(t)
}
func TestObservableChanRecordChannelCapacity(t *testing.T) {
t.Parallel()
mockMetrics := new(MockMetricsCollector)
bufferCap := 10
mockMetrics.On("RecordChannelCap", bufferCap).Twice()
mockMetrics.On("RecordChannelLen", mock.AnythingOfType("int")).Once()
ch := make(chan int, bufferCap)
oc := NewObservableChan(ch, mockMetrics)
oc.RecordChannelCapacity()
mockMetrics.AssertExpectations(t)
}
func TestObservableChanRecordChannelLen(t *testing.T) {
t.Parallel()
mockMetrics := new(MockMetricsCollector)
bufferCap := 10
mockMetrics.On("RecordChannelLen", mock.AnythingOfType("int")).Twice()
mockMetrics.On("RecordChannelCap", bufferCap).Once()
ch := make(chan int, bufferCap)
oc := NewObservableChan(ch, mockMetrics)
oc.RecordChannelLen()
mockMetrics.AssertExpectations(t)
}
func TestObservableChan_Close(t *testing.T) {
t.Parallel()
mockMetrics := new(MockMetricsCollector)
bufferCap := 1
mockMetrics.On("RecordChannelCap", bufferCap).Once()
mockMetrics.On("RecordChannelLen", mock.AnythingOfType("int")).Twice()
ch := make(chan int, bufferCap)
oc := NewObservableChan(ch, mockMetrics)
oc.Close()
mockMetrics.AssertExpectations(t)
}
func TestObservableChanClosed(t *testing.T) {
t.Parallel()
ch := make(chan int)
close(ch)
oc := NewObservableChan(ch, nil)
ctx, cancel := context.WithCancel(context.Background())
// Closed channel should return with an error.
v, err := oc.RecvCtx(ctx)
assert.Error(t, err)
assert.Equal(t, 0, v)
// Cancelled context should also return with an error.
cancel()
_, err = oc.RecvCtx(ctx)
assert.Error(t, err)
}
================================================
FILE: pkg/cleantemp/cleantemp.go
================================================
package cleantemp
import (
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/mitchellh/go-ps"
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
const (
defaultExecPath = "trufflehog"
defaultArtifactPrefixFormat = "%s-%d-"
)
// MkdirTemp returns a temporary directory path formatted as:
// trufflehog--
func MkdirTemp() (string, error) {
pid := os.Getpid()
tmpdir := fmt.Sprintf(defaultArtifactPrefixFormat, defaultExecPath, pid)
dir, err := os.MkdirTemp(os.TempDir(), tmpdir)
if err != nil {
return "", err
}
return dir, nil
}
// Unlike MkdirTemp, we only want to generate the filename string.
// The tempfile creation in trufflehog we're interested in
// is generally handled by "github.com/trufflesecurity/disk-buffer-reader"
func MkFilename() string {
pid := os.Getpid()
filename := fmt.Sprintf(defaultArtifactPrefixFormat, defaultExecPath, pid)
return filename
}
// Only compile during startup.
var trufflehogRE = regexp.MustCompile(`^trufflehog-\d+-\d+$`)
// CleanTempArtifacts deletes orphaned temp directories and files that do not contain running PID values.
func CleanTempArtifacts(ctx logContext.Context) error {
executablePath, err := os.Executable()
if err != nil {
executablePath = defaultExecPath
}
execName := filepath.Base(executablePath)
var pids []string
procs, err := ps.Processes()
if err != nil {
return fmt.Errorf("error getting jobs PIDs: %w", err)
}
for _, proc := range procs {
if proc.Executable() == execName {
pids = append(pids, strconv.Itoa(proc.Pid()))
}
}
if len(pids) == 0 {
ctx.Logger().V(5).Info("No trufflehog processes were found")
return nil
}
tempDir := os.TempDir()
dir, err := os.Open(tempDir)
if err != nil {
return fmt.Errorf("error opening temp dir: %w", err)
}
defer dir.Close()
for {
entries, err := dir.ReadDir(1) // read only one entry
if err != nil {
if err == io.EOF {
break
}
continue
}
entry := entries[0]
if trufflehogRE.MatchString(entry.Name()) {
// Mark these artifacts initially as ones that should be deleted.
shouldDelete := true
// Check if the name matches any live PIDs.
// Potential race condition here if a PID is started and creates tmp data after the initial check.
for _, pidval := range pids {
if strings.Contains(entry.Name(), fmt.Sprintf("-%s-", pidval)) {
shouldDelete = false
break
}
}
if shouldDelete {
path := filepath.Join(tempDir, entry.Name())
isDir := entry.IsDir()
if isDir {
err = os.RemoveAll(path)
} else {
err = os.Remove(path)
}
if err != nil {
return fmt.Errorf("error deleting temp artifact (dir: %v) %s: %w", isDir, path, err)
}
ctx.Logger().V(4).Info("Deleted orphaned temp artifact", "artifact", path)
}
}
}
return nil
}
// CleanTempDirsForLegacyJSON removes all directories that start with "trufflehog-"
// from either the provided clonePath (if not empty) or the OS temp directory.
func CleanTempDirsForLegacyJSON(baseDir string) error {
// If no custom clone path was provided, clean repos from the OS temp directory
// since that's where they were cloned during the scan.
if baseDir == "" {
baseDir = os.TempDir()
}
entries, err := os.ReadDir(baseDir)
if err != nil {
return err
}
for _, entry := range entries {
if entry.IsDir() && strings.HasPrefix(entry.Name(), "trufflehog-") {
fullPath := filepath.Join(baseDir, entry.Name())
if err := os.RemoveAll(fullPath); err != nil {
return err
}
}
}
return nil
}
================================================
FILE: pkg/cleantemp/cleantemp_test.go
================================================
package cleantemp
import (
"os"
"path/filepath"
"testing"
"github.com/mitchellh/go-ps"
"github.com/stretchr/testify/assert"
)
func TestExecName(t *testing.T) {
executablePath, err := os.Executable()
assert.Nil(t, err)
execName := filepath.Base(executablePath)
assert.Equal(t, "cleantemp.test", execName)
procs, err := ps.Processes()
assert.Nil(t, err)
assert.NotEmpty(t, procs)
found := false
for _, proc := range procs {
if proc.Executable() == execName {
found = true
}
}
assert.True(t, found)
}
func TestCleanTempDirsForLegacyJSON(t *testing.T) {
baseDir := t.TempDir()
// Create dirs that should be deleted
dir1 := filepath.Join(baseDir, "trufflehog-123")
dir2 := filepath.Join(baseDir, "trufflehog-456")
assert.NoError(t, os.Mkdir(dir1, 0o755))
assert.NoError(t, os.Mkdir(dir2, 0o755))
// Create dirs that should NOT be deleted
keepDir := filepath.Join(baseDir, "keepme-123")
assert.NoError(t, os.Mkdir(keepDir, 0o755))
// Create a file with trufflehog- prefix (should not be deleted because only dirs are deleted)
keepFile := filepath.Join(baseDir, "trufflehog-file")
assert.NoError(t, os.WriteFile(keepFile, []byte("data"), 0o644))
err := CleanTempDirsForLegacyJSON(baseDir)
assert.NoError(t, err)
_, err = os.Stat(dir1)
assert.True(t, os.IsNotExist(err))
_, err = os.Stat(dir2)
assert.True(t, os.IsNotExist(err))
_, err = os.Stat(keepDir)
assert.NoError(t, err)
_, err = os.Stat(keepFile)
assert.NoError(t, err)
}
================================================
FILE: pkg/common/context.go
================================================
package common
import "context"
// ChannelClosedErr indicates that a read was performed from a closed channel.
type ChannelClosedErr struct{}
func (ChannelClosedErr) Error() string { return "channel is closed" }
func IsDone(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}
// CancellableWrite blocks on writing the item to the channel but can be
// cancelled by the context. If both the context is cancelled and the channel
// write would succeed, either operation will be performed randomly, however
// priority is given to context cancellation.
func CancellableWrite[T any](ctx context.Context, ch chan<- T, item T) error {
select {
case <-ctx.Done(): // priority to context cancellation
return ctx.Err()
default:
select {
case <-ctx.Done():
return ctx.Err()
case ch <- item:
return nil
}
}
}
// CancellableRead blocks on receiving an item from the channel but can be
// cancelled by the context. If the channel is closed, a ChannelClosedErr is
// returned. If both the context is cancelled and the channel read would
// succeed, either operation will be performed randomly, however priority is
// given to context cancellation.
func CancellableRead[T any](ctx context.Context, ch <-chan T) (T, error) {
var zero T // zero value of type T
select {
case <-ctx.Done(): // priority to context cancellation
return zero, ctx.Err()
default:
select {
case <-ctx.Done():
return zero, ctx.Err()
case item, ok := <-ch:
if !ok {
return item, ChannelClosedErr{}
}
return item, nil
}
}
}
================================================
FILE: pkg/common/export_error.go
================================================
package common
// ExportError is an implementation of error that can be JSON marshalled. It
// must be a public exported type for this reason.
type ExportError string
func (e ExportError) Error() string { return string(e) }
// ExportErrors converts a list of errors into []ExportError.
func ExportErrors(errs ...error) []error {
output := make([]error, 0, len(errs))
for _, err := range errs {
output = append(output, ExportError(err.Error()))
}
return output
}
================================================
FILE: pkg/common/filter.go
================================================
package common
import (
"bufio"
"fmt"
"os"
"regexp"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
type Filter struct {
include *FilterRuleSet
exclude *FilterRuleSet
}
type FilterRuleSet []regexp.Regexp
// FilterEmpty returns a Filter that always passes.
func FilterEmpty() *Filter {
filter, err := FilterFromFiles("", "")
if err != nil {
context.Background().Logger().Error(err, "could not create empty filter")
os.Exit(1)
}
return filter
}
// FilterFromFiles creates a Filter using the rules in the provided include and exclude files.
func FilterFromFiles(includeFilterPath, excludeFilterPath string) (*Filter, error) {
includeRules, err := FilterRulesFromFile(includeFilterPath)
if err != nil {
return nil, fmt.Errorf("could not create include rules: %s", err)
}
excludeRules, err := FilterRulesFromFile(excludeFilterPath)
if err != nil {
return nil, fmt.Errorf("could not create exclude rules: %s", err)
}
// If no includeFilterPath is provided, every pattern should pass the include rules.
if includeFilterPath == "" {
includeRules = &FilterRuleSet{*regexp.MustCompile("")}
}
filter := &Filter{
include: includeRules,
exclude: excludeRules,
}
return filter, nil
}
// FilterRulesFromFile loads the list of regular expression filter rules in `source` and creates a FilterRuleSet.
func FilterRulesFromFile(source string) (*FilterRuleSet, error) {
rules := FilterRuleSet{}
if source == "" {
return &rules, nil
}
commentPattern := regexp.MustCompile(`^\s*#`)
emptyLinePattern := regexp.MustCompile(`^\s*$`)
file, err := os.Open(source)
logger := context.Background().Logger().WithValues("file", source)
if err != nil {
logger.Error(err, "unable to open filter file", "file", source)
os.Exit(1)
}
defer func(file *os.File) {
err := file.Close()
if err != nil {
logger.Error(err, "unable to close filter file")
os.Exit(1)
}
}(file)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if commentPattern.MatchString(line) {
continue
}
if emptyLinePattern.MatchString(line) {
continue
}
pattern, err := regexp.Compile(line)
if err != nil {
return nil, fmt.Errorf("can not compile regular expression: %s", line)
}
rules = append(rules, *pattern)
}
return &rules, nil
}
// Pass returns true if the include FilterRuleSet matches the pattern and the exclude FilterRuleSet does not match.
func (filter *Filter) Pass(object string) bool {
if filter == nil {
return true
}
excluded := filter.exclude.Matches(object)
included := filter.include.Matches(object)
return !excluded && included
}
// Matches will return true if any of the regular expressions in the FilterRuleSet match the pattern.
func (rules *FilterRuleSet) Matches(object string) bool {
if rules == nil {
return false
}
for _, rule := range *rules {
if rule.MatchString(object) {
return true
}
}
return false
}
// ShouldExclude return true if any regular expressions in the exclude FilterRuleSet matches the path.
func (filter *Filter) ShouldExclude(path string) bool {
return filter.exclude.Matches(path)
}
================================================
FILE: pkg/common/filter_test.go
================================================
package common
import (
"os"
"regexp"
"testing"
)
func TestFilterBasic(t *testing.T) {
type filterTest struct {
filter Filter
pattern string
pass bool
}
tests := map[string]filterTest{
"IncludePassed": {
filter: Filter{
include: &FilterRuleSet{*regexp.MustCompile("test")},
},
pattern: "teststring",
pass: true,
},
"IncludeFiltered": {
filter: Filter{
include: &FilterRuleSet{*regexp.MustCompile("nomatch")},
},
pattern: "teststring",
pass: false,
},
"ExcludePassed": {
filter: Filter{
include: &FilterRuleSet{*regexp.MustCompile("")},
exclude: &FilterRuleSet{*regexp.MustCompile("nomatch")},
},
pattern: "teststring",
pass: true,
},
"ExcludeFiltered": {
filter: Filter{
include: &FilterRuleSet{*regexp.MustCompile("")},
exclude: &FilterRuleSet{*regexp.MustCompile("test")},
},
pattern: "teststring",
pass: false,
},
"IncludeExcludeDifferentPass": {
filter: Filter{
include: &FilterRuleSet{*regexp.MustCompile("test")},
exclude: &FilterRuleSet{*regexp.MustCompile("nomatch")},
},
pattern: "teststring",
pass: true,
},
"IncludeExcludeDifferentFiltered": {
filter: Filter{
include: &FilterRuleSet{*regexp.MustCompile("nomatch")},
exclude: &FilterRuleSet{*regexp.MustCompile("test")},
},
pattern: "teststring",
pass: false,
},
"IncludeExcludeSameFiltered": {
filter: Filter{
include: &FilterRuleSet{*regexp.MustCompile("test")},
exclude: &FilterRuleSet{*regexp.MustCompile("test")},
},
pattern: "teststring",
pass: false,
},
}
for name, test := range tests {
if test.filter.Pass(test.pattern) != test.pass {
t.Errorf("%s: unexpected filter result. pattern: %q, pass: %t", name, test.pattern, !test.pass)
}
}
}
func TestFilterFromFile(t *testing.T) {
type filterTest struct {
includeFile bool
excludeFile bool
includeFileContents string
excludeFileContents string
pattern string
pass bool
}
tests := map[string]filterTest{
"includeFileOnlyPass": {
includeFile: true,
excludeFile: false,
includeFileContents: "test",
pattern: "test",
pass: true,
},
"includeFileOnlyFiltered": {
includeFile: true,
excludeFile: false,
includeFileContents: "nomatch",
pattern: "test",
pass: false,
},
"includeFileEmptyFiltered": {
includeFile: true,
excludeFile: false,
includeFileContents: "",
pattern: "test",
pass: false,
},
"excludeFileOnlyPass": {
includeFile: false,
excludeFile: true,
excludeFileContents: "nomatch",
pattern: "test",
pass: true,
},
"excludeFileOnlyFiltered": {
includeFile: false,
excludeFile: true,
excludeFileContents: "test",
pattern: "test",
pass: false,
},
"BothFilesEmptyExcludeFiltered": {
includeFile: true,
excludeFile: true,
excludeFileContents: "",
includeFileContents: "",
pattern: "test",
pass: false,
},
"EmptyLinesAreIgnored": {
includeFile: false,
excludeFile: true,
excludeFileContents: " \ntest.txt",
pattern: "hello world.txt",
pass: true,
},
}
for name, test := range tests {
var includeTestFile, excludeTestFile string
if test.includeFile {
includeTestFile = "/tmp/trufflehog_test_ifilter.txt"
if err := testFilterWriteFile(includeTestFile, []byte(test.includeFileContents)); err != nil {
t.Fatalf("failed to create include rules file: %s", err)
}
defer os.Remove(includeTestFile)
}
if test.excludeFile {
excludeTestFile = "/tmp/trufflehog_test_xfilter.txt"
if err := testFilterWriteFile(excludeTestFile, []byte(test.excludeFileContents)); err != nil {
t.Fatalf("failed to create include rules file: %s", err)
}
defer os.Remove(excludeTestFile)
}
filter, err := FilterFromFiles(includeTestFile, excludeTestFile)
if err != nil {
t.Errorf("failed to create filter from files: %s", err)
}
if filter.Pass(test.pattern) != test.pass {
t.Errorf("%s: unexpected filter result. pattern: %q, pass: %t", name, test.pattern, !test.pass)
}
}
}
func testFilterWriteFile(filename string, content []byte) error {
f, err := os.Create(filename)
if err != nil {
return err
}
_, err = f.Write(content)
if err != nil {
return err
}
return f.Close()
}
================================================
FILE: pkg/common/glob/glob.go
================================================
package glob
import (
"fmt"
"github.com/gobwas/glob"
)
// Filter is a generic filter for excluding and including globs (limited
// regular expressions). Exclusion takes precedence if both include and exclude
// lists are provided.
type Filter struct {
exclude []glob.Glob
include []glob.Glob
}
type globFilterOpt func(*Filter) error
// WithExcludeGlobs adds exclude globs to the filter.
func WithExcludeGlobs(excludes ...string) globFilterOpt {
return func(f *Filter) error {
for _, exclude := range excludes {
g, err := glob.Compile(exclude)
if err != nil {
return fmt.Errorf("invalid exclude glob %q: %w", exclude, err)
}
f.exclude = append(f.exclude, g)
}
return nil
}
}
// WithIncludeGlobs adds include globs to the filter.
func WithIncludeGlobs(includes ...string) globFilterOpt {
return func(f *Filter) error {
for _, include := range includes {
g, err := glob.Compile(include)
if err != nil {
return fmt.Errorf("invalid include glob %q: %w", include, err)
}
f.include = append(f.include, g)
}
return nil
}
}
// NewGlobFilter creates a new Filter with the provided options.
func NewGlobFilter(opts ...globFilterOpt) (*Filter, error) {
filter := &Filter{}
for _, opt := range opts {
if err := opt(filter); err != nil {
return nil, err
}
}
return filter, nil
}
// ShouldInclude returns whether the object is in the include list or not in
// the exclude list (exclude taking precedence).
func (f *Filter) ShouldInclude(object string) bool {
if f == nil {
return true
}
exclude, include := len(f.exclude), len(f.include)
if exclude == 0 && include == 0 {
return true
} else if exclude > 0 && include == 0 {
return f.shouldIncludeFromExclude(object)
} else if exclude == 0 && include > 0 {
return f.shouldIncludeFromInclude(object)
} else {
if ok, err := f.shouldIncludeFromBoth(object); err == nil {
return ok
}
// Ambiguous case.
return false
}
}
// shouldIncludeFromExclude checks for explicitly excluded paths. This should
// only be called when the include list is empty.
func (f *Filter) shouldIncludeFromExclude(object string) bool {
for _, g := range f.exclude {
if g.Match(object) {
return false
}
}
return true
}
// shouldIncludeFromInclude checks for explicitly included paths. This should
// only be called when the exclude list is empty.
func (f *Filter) shouldIncludeFromInclude(object string) bool {
for _, g := range f.include {
if g.Match(object) {
return true
}
}
return false
}
// shouldIncludeFromBoth checks for either excluded or included paths. Exclusion
// takes precedence. If neither list contains the object, true is returned.
func (f *Filter) shouldIncludeFromBoth(object string) (bool, error) {
// Exclude takes precedence. If we find the object in the exclude list,
// we should not match.
for _, g := range f.exclude {
if g.Match(object) {
return false, nil
}
}
// If we find the object in the include list, we should match.
for _, g := range f.include {
if g.Match(object) {
return true, nil
}
}
// If we find it in neither, return an error to let the caller decide.
return false, fmt.Errorf("ambiguous match")
}
================================================
FILE: pkg/common/glob/glob_test.go
================================================
package glob
import (
"testing"
"github.com/stretchr/testify/assert"
"pgregory.net/rapid"
)
type globTest struct {
input string
shouldInclude bool
}
func testGlobs(t *testing.T, filter *Filter, tests ...globTest) {
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
// Invert because mentally it's easier to say whether an
// input should be included.
assert.Equal(t, tt.shouldInclude, filter.ShouldInclude(tt.input))
})
}
}
func TestGlobFilterExclude(t *testing.T) {
filter, err := NewGlobFilter(WithExcludeGlobs("foo", "bar*"))
assert.NoError(t, err)
testGlobs(t, filter,
globTest{"foo", false},
globTest{"bar", false},
globTest{"bara", false},
globTest{"barb", false},
globTest{"barbosa", false},
globTest{"foobar", true},
globTest{"food", true},
globTest{"anything else", true},
)
}
func TestGlobFilterInclude(t *testing.T) {
filter, err := NewGlobFilter(WithIncludeGlobs("foo", "bar*"))
assert.NoError(t, err)
testGlobs(t, filter,
globTest{"foo", true},
globTest{"bar", true},
globTest{"bara", true},
globTest{"barb", true},
globTest{"barbosa", true},
globTest{"foobar", false},
globTest{"food", false},
globTest{"anything else", false},
)
}
func TestGlobFilterEmpty(t *testing.T) {
filter, err := NewGlobFilter()
assert.NoError(t, err)
testGlobs(t, filter,
globTest{"foo", true},
globTest{"bar", true},
globTest{"bara", true},
globTest{"barb", true},
globTest{"barbosa", true},
globTest{"foobar", true},
globTest{"food", true},
globTest{"anything else", true},
)
}
func TestGlobFilterExcludeInclude(t *testing.T) {
filter, err := NewGlobFilter(WithExcludeGlobs("/foo/bar/**"), WithIncludeGlobs("/foo/**"))
assert.NoError(t, err)
testGlobs(t, filter,
globTest{"/foo/a", true},
globTest{"/foo/b", true},
globTest{"/foo/c/d/e", true},
globTest{"/foo/bar/a", false},
globTest{"/foo/bar/b", false},
globTest{"/foo/bar/c/d/e", false},
globTest{"/any/other/path", false},
)
}
func TestGlobFilterExcludePrecedence(t *testing.T) {
filter, err := NewGlobFilter(WithExcludeGlobs("foo"), WithIncludeGlobs("foo*"))
assert.NoError(t, err)
testGlobs(t, filter,
globTest{"foo", false},
globTest{"foobar", true},
)
}
func TestGlobErrorContainsGlob(t *testing.T) {
invalidGlob := "[this is invalid because it doesn't close the capture group"
_, err := NewGlobFilter(WithExcludeGlobs(invalidGlob))
assert.Error(t, err)
assert.Contains(t, err.Error(), invalidGlob)
}
// The filters in this test should be mutually exclusive because one includes
// and the other excludes the same glob.
func TestGlobInverse(t *testing.T) {
for _, glob := range []string{
"a",
"a*",
"a**",
"*a",
"**a",
"*",
} {
include, err := NewGlobFilter(WithIncludeGlobs(glob))
assert.NoError(t, err)
exclude, err := NewGlobFilter(WithExcludeGlobs(glob))
assert.NoError(t, err)
rapid.Check(t, func(t *rapid.T) {
input := rapid.String().Draw(t, "input")
a, b := include.ShouldInclude(input), exclude.ShouldInclude(input)
if a == b {
t.Fatalf("Filter(Include(%q)) == Filter(Exclude(%q)) == %v for input %q", glob, glob, a, input)
}
})
}
}
func TestGlobDefaultFilters(t *testing.T) {
for _, filter := range []*Filter{nil, {}} {
rapid.Check(t, func(t *rapid.T) {
if !filter.ShouldInclude(rapid.String().Draw(t, "input")) {
t.Fatalf("filter %#v did not include input", filter)
}
})
}
}
================================================
FILE: pkg/common/http.go
================================================
package common
import (
"crypto/tls"
"crypto/x509"
"io"
"net"
"net/http"
"strings"
"time"
"github.com/hashicorp/go-retryablehttp"
"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
)
var caCerts = []string{
// CN = ISRG Root X1
// TODO: Expires Monday, June 4, 2035 at 4:04:38 AM Pacific
`
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
`,
// CN = ISRG Root X2
// TODO: Expires September 17, 2040 at 9:00:00 AM Pacific Daylight Time
`
-----BEGIN CERTIFICATE-----
MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
/q4AaOeMSQ+2b1tbFfLn
-----END CERTIFICATE-----
`,
}
func PinnedCertPool() *x509.CertPool {
trustedCerts := x509.NewCertPool()
for _, cert := range caCerts {
trustedCerts.AppendCertsFromPEM([]byte(strings.TrimSpace(cert)))
}
return trustedCerts
}
type FakeTransport struct {
CreateResponse func(req *http.Request) (*http.Response, error)
}
func (t FakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return t.CreateResponse(req)
}
type CustomTransport struct {
T http.RoundTripper
}
func UserAgent() string {
if len(feature.UserAgentSuffix.Load()) > 0 {
return "TruffleHog " + feature.UserAgentSuffix.Load()
}
return "TruffleHog"
}
func (t *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Add("User-Agent", UserAgent())
return t.T.RoundTrip(req)
}
func NewCustomTransport(T http.RoundTripper) *CustomTransport {
if T == nil {
T = http.DefaultTransport
}
return &CustomTransport{T}
}
type InstrumentedTransport struct {
T http.RoundTripper
}
func (t *InstrumentedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
sanitizedURL := sanitizeURL(req.URL.String())
// increment counter for the URL
recordHTTPRequest(sanitizedURL)
// Record start time for latency measurement
start := time.Now()
resp, err := t.T.RoundTrip(req)
// Time the latency
duration := time.Since(start)
if err != nil {
recordNetworkError(sanitizedURL)
return nil, err
}
if resp != nil {
// record latency, response size and increment counter for non-200 status code
recordHTTPResponse(sanitizedURL, resp.StatusCode, duration.Seconds(), resp.ContentLength)
}
return resp, err
}
func NewInstrumentedTransport(T http.RoundTripper) *InstrumentedTransport {
if T == nil {
T = http.DefaultTransport
}
return &InstrumentedTransport{T}
}
func ConstantResponseHttpClient(statusCode int, body string) *http.Client {
return &http.Client{
Timeout: DefaultResponseTimeout,
Transport: FakeTransport{
CreateResponse: func(req *http.Request) (*http.Response, error) {
return &http.Response{
Request: req,
Body: io.NopCloser(strings.NewReader(body)),
StatusCode: statusCode,
}, nil
},
},
}
}
// ClientOption configures how we set up the client.
type ClientOption func(*retryablehttp.Client)
// WithCheckRetry allows setting a custom CheckRetry policy.
func WithCheckRetry(cr retryablehttp.CheckRetry) ClientOption {
return func(c *retryablehttp.Client) { c.CheckRetry = cr }
}
// WithBackoff allows setting a custom backoff policy.
func WithBackoff(b retryablehttp.Backoff) ClientOption {
return func(c *retryablehttp.Client) { c.Backoff = b }
}
// WithTimeout allows setting a custom timeout.
func WithTimeout(timeout time.Duration) ClientOption {
return func(c *retryablehttp.Client) { c.HTTPClient.Timeout = timeout }
}
// WithMaxRetries allows setting a custom maximum number of retries.
func WithMaxRetries(retries int) ClientOption {
return func(c *retryablehttp.Client) { c.RetryMax = retries }
}
// WithRetryWaitMin allows setting a custom minimum retry wait.
func WithRetryWaitMin(wait time.Duration) ClientOption {
return func(c *retryablehttp.Client) { c.RetryWaitMin = wait }
}
// WithRetryWaitMax allows setting a custom maximum retry wait.
func WithRetryWaitMax(wait time.Duration) ClientOption {
return func(c *retryablehttp.Client) { c.RetryWaitMax = wait }
}
func PinnedRetryableHttpClient() *http.Client {
httpClient := retryablehttp.NewClient()
httpClient.Logger = nil
httpClient.HTTPClient.Transport = NewInstrumentedTransport(NewCustomTransport(&http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: PinnedCertPool(),
},
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}))
return httpClient.StandardClient()
}
func RetryableHTTPClient(opts ...ClientOption) *http.Client {
httpClient := retryablehttp.NewClient()
httpClient.RetryMax = 3
httpClient.Logger = nil
httpClient.HTTPClient.Transport = NewInstrumentedTransport(NewCustomTransport(nil))
for _, opt := range opts {
opt(httpClient)
}
return httpClient.StandardClient()
}
// RetryableHTTPClientTimeout returns a new http client with a specified timeout and RoundTripper transport
func RetryableHTTPClientTimeout(timeOutSeconds int64, opts ...ClientOption) *http.Client {
httpClient := retryablehttp.NewClient()
httpClient.RetryMax = 3
httpClient.Logger = nil
httpClient.HTTPClient.Timeout = time.Duration(timeOutSeconds) * time.Second
httpClient.HTTPClient.Transport = NewInstrumentedTransport(NewCustomTransport(nil))
for _, opt := range opts {
opt(httpClient)
}
standardClient := httpClient.StandardClient()
standardClient.Timeout = httpClient.HTTPClient.Timeout
return standardClient
}
const DefaultResponseTimeout = 5 * time.Second
var saneTransport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 2 * time.Second,
KeepAlive: 5 * time.Second,
}).DialContext,
MaxIdleConns: 5,
IdleConnTimeout: 5 * time.Second,
TLSHandshakeTimeout: 3 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
func SaneHttpClient() *http.Client {
httpClient := &http.Client{}
httpClient.Timeout = DefaultResponseTimeout
httpClient.Transport = NewInstrumentedTransport(NewCustomTransport(saneTransport))
return httpClient
}
// SaneHttpClientTimeOut adds a custom timeout for some scanners
func SaneHttpClientTimeOut(timeout time.Duration) *http.Client {
httpClient := &http.Client{}
httpClient.Timeout = timeout
httpClient.Transport = NewInstrumentedTransport(NewCustomTransport(nil))
return httpClient
}
================================================
FILE: pkg/common/http_metrics.go
================================================
package common
import (
"net/url"
"strconv"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
httpRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: "http_client",
Name: "requests_total",
Help: "Total number of HTTP requests made, labeled by URL.",
},
[]string{"url"},
)
httpRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: "http_client",
Name: "request_duration_seconds",
Help: "HTTP request latency in seconds, labeled by URL.",
Buckets: prometheus.DefBuckets,
},
[]string{"url"},
)
httpNon200ResponsesTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: "http_client",
Name: "non_200_responses_total",
Help: "Total number of non-200 HTTP responses, labeled by URL and status code.",
},
[]string{"url", "status_code"},
)
httpResponseBodySizeBytes = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: "http_client",
Name: "response_body_size_bytes",
Help: "Size of HTTP response bodies in bytes, labeled by URL.",
Buckets: prometheus.ExponentialBuckets(100, 10, 5), // [100B, 1KB, 10KB, 100KB, 1MB]
},
[]string{"url"},
)
)
// sanitizeURL sanitizes a URL to avoid high cardinality metrics.
// It keeps only the host and path, removing query parameters, fragments, and user info.
func sanitizeURL(rawURL string) string {
if rawURL == "" {
return "unknown"
}
parsedURL, err := url.Parse(rawURL)
if err != nil {
return "invalid_url"
}
// Build sanitized URL with just scheme, host, and path
sanitized := &url.URL{
Scheme: parsedURL.Scheme,
Host: parsedURL.Host,
Path: parsedURL.Path,
}
// If host is empty, try to extract from the raw URL
if sanitized.Host == "" {
// For relative URLs or malformed URLs, just use a placeholder
return "relative_or_invalid"
}
// Normalize path
if sanitized.Path == "" {
sanitized.Path = "/"
}
// Limit path length to avoid extremely long paths creating high cardinality
if len(sanitized.Path) > 100 {
sanitized.Path = sanitized.Path[:100] + "..."
}
result := sanitized.String()
// Final fallback to avoid empty strings
if result == "" {
return "unknown"
}
return result
}
// recordHTTPRequest records metrics for an HTTP request.
func recordHTTPRequest(sanitizedURL string) {
httpRequestsTotal.WithLabelValues(sanitizedURL).Inc()
}
// recordHTTPResponse records metrics for an HTTP response.
func recordHTTPResponse(sanitizedURL string, statusCode int, durationSeconds float64, contentLength int64) {
// Record latency
httpRequestDuration.WithLabelValues(sanitizedURL).Observe(durationSeconds)
// Record non-200 responses
if statusCode != 200 {
httpNon200ResponsesTotal.WithLabelValues(sanitizedURL, strconv.Itoa(statusCode)).Inc()
}
// Record response body size if known
if contentLength >= 0 {
httpResponseBodySizeBytes.WithLabelValues(sanitizedURL).Observe(float64(contentLength))
}
}
// recordNetworkError records metrics for failed HTTP response
func recordNetworkError(sanitizedURL string) {
httpNon200ResponsesTotal.WithLabelValues(sanitizedURL, "network_error").Inc()
}
================================================
FILE: pkg/common/http_test.go
================================================
package common
import (
"context"
"math"
"net/http"
"net/http/httptest"
"slices"
"strings"
"testing"
"time"
"github.com/hashicorp/go-retryablehttp"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRetryableHTTPClientCheckRetry(t *testing.T) {
testCases := []struct {
name string
responseStatus int
checkRetry retryablehttp.CheckRetry
expectedRetries int
}{
{
name: "Retry on 500 status, give up after 3 retries",
responseStatus: http.StatusInternalServerError, // Server error status
checkRetry: func(ctx context.Context, resp *http.Response, err error) (bool, error) {
if err != nil {
t.Errorf("expected response with 500 status, got error: %v", err)
return false, err
}
// The underlying transport will retry on 500 status.
if resp.StatusCode == http.StatusInternalServerError {
return true, nil
}
return false, nil
},
expectedRetries: 3,
},
{
name: "No retry on 400 status",
responseStatus: http.StatusBadRequest, // Client error status
checkRetry: func(ctx context.Context, resp *http.Response, err error) (bool, error) {
// Do not retry on client errors.
return false, nil
},
expectedRetries: 0,
},
{
name: "Retry on 429 status, give up after 3 retries",
responseStatus: http.StatusTooManyRequests,
checkRetry: func(ctx context.Context, resp *http.Response, err error) (bool, error) {
if err != nil {
t.Errorf("expected response with 429 status, got error: %v", err)
return false, err
}
// The underlying transport will retry on 429 status.
if resp.StatusCode == http.StatusTooManyRequests {
return true, nil
}
return false, nil
},
expectedRetries: 3,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var retryCount int
// Do not count the initial request as a retry.
i := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if i != 0 {
retryCount++
}
i++
w.WriteHeader(tc.responseStatus)
}))
defer server.Close()
ctx := context.Background()
client := RetryableHTTPClient(WithCheckRetry(tc.checkRetry), WithTimeout(10*time.Millisecond), WithRetryWaitMin(1*time.Millisecond))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil)
assert.NoError(t, err)
// Bad linter, there is no body to close.
_, err = client.Do(req) //nolint:bodyclose
if slices.Contains([]int{http.StatusInternalServerError, http.StatusTooManyRequests}, tc.responseStatus) {
// The underlying transport will retry on 500 and 429 status.
assert.Error(t, err)
}
assert.Equal(t, tc.expectedRetries, retryCount, "Retry count does not match expected")
})
}
}
func TestRetryableHTTPClientMaxRetry(t *testing.T) {
testCases := []struct {
name string
responseStatus int
maxRetries int
expectedRetries int
}{
{
name: "Max retries with 500 status",
responseStatus: http.StatusInternalServerError,
maxRetries: 2,
expectedRetries: 2,
},
{
name: "Max retries with 429 status",
responseStatus: http.StatusTooManyRequests,
maxRetries: 1,
expectedRetries: 1,
},
{
name: "Max retries with 200 status",
responseStatus: http.StatusOK,
maxRetries: 3,
expectedRetries: 0,
},
{
name: "Max retries with 400 status",
responseStatus: http.StatusBadRequest,
maxRetries: 3,
expectedRetries: 0,
},
{
name: "Max retries with 401 status",
responseStatus: http.StatusUnauthorized,
maxRetries: 3,
expectedRetries: 0,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var retryCount int
i := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if i != 0 {
retryCount++
}
i++
w.WriteHeader(tc.responseStatus)
}))
defer server.Close()
client := RetryableHTTPClient(
WithMaxRetries(tc.maxRetries),
WithTimeout(10*time.Millisecond),
WithRetryWaitMin(1*time.Millisecond),
)
ctx := context.Background()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil)
assert.NoError(t, err)
// Bad linter, there is no body to close.
_, err = client.Do(req) //nolint:bodyclose
if err != nil && tc.responseStatus == http.StatusOK {
assert.Error(t, err)
}
assert.Equal(t, tc.expectedRetries, retryCount, "Retry count does not match expected")
})
}
}
func TestRetryableHTTPClientBackoff(t *testing.T) {
testCases := []struct {
name string
responseStatus int
expectedRetries int
backoffPolicy retryablehttp.Backoff
expectedBackoffs []time.Duration
}{
{
name: "Custom backoff on 500 status",
responseStatus: http.StatusInternalServerError,
expectedRetries: 3,
backoffPolicy: func(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
switch attemptNum {
case 1:
return 1 * time.Millisecond
case 2:
return 2 * time.Millisecond
case 3:
return 4 * time.Millisecond
default:
return max
}
},
expectedBackoffs: []time.Duration{1 * time.Millisecond, 2 * time.Millisecond, 4 * time.Millisecond},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var actualBackoffs []time.Duration
var lastTime time.Time
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
now := time.Now()
if !lastTime.IsZero() {
actualBackoffs = append(actualBackoffs, now.Sub(lastTime))
}
lastTime = now
w.WriteHeader(tc.responseStatus)
}))
defer server.Close()
ctx := context.Background()
client := RetryableHTTPClient(
WithBackoff(tc.backoffPolicy),
WithTimeout(10*time.Millisecond),
WithRetryWaitMin(1*time.Millisecond),
WithRetryWaitMax(10*time.Millisecond),
)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil)
assert.NoError(t, err)
_, err = client.Do(req) //nolint:bodyclose
assert.Error(t, err, "Expected error due to 500 status")
assert.Len(t, actualBackoffs, tc.expectedRetries, "Unexpected number of backoffs")
for i, expectedBackoff := range tc.expectedBackoffs {
if i < len(actualBackoffs) {
// Allow some deviation in timing due to processing delays.
assert.Less(t, math.Abs(float64(actualBackoffs[i]-expectedBackoff)), float64(15*time.Millisecond), "Unexpected backoff duration")
}
}
})
}
}
func TestRetryableHTTPClientTimeout(t *testing.T) {
testCases := []struct {
name string
timeoutSeconds int64
expectedTimeout time.Duration
}{
{
name: "5 second timeout",
timeoutSeconds: 5,
expectedTimeout: 5 * time.Second,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Call the function with the test timeout value
client := RetryableHTTPClientTimeout(tc.timeoutSeconds)
// Verify that the timeout is set correctly
assert.Equal(t, tc.expectedTimeout, client.Timeout, "HTTP client timeout does not match expected value")
// Verify that the transport is a custom transport
_, isRoundTripperTransport := client.Transport.(*retryablehttp.RoundTripper)
assert.True(t, isRoundTripperTransport, "HTTP client transport is not a retryablehttp.RoundTripper")
})
}
}
func TestSanitizeURL(t *testing.T) {
testCases := []struct {
name string
input string
expected string
}{
{
name: "valid https URL",
input: "https://api.example.com/v1/users",
expected: "https://api.example.com/v1/users",
},
{
name: "URL with query parameters",
input: "https://api.example.com/search?q=secret&limit=10",
expected: "https://api.example.com/search",
},
{
name: "URL with fragment",
input: "https://example.com/page#section",
expected: "https://example.com/page",
},
{
name: "URL with user info",
input: "https://user:pass@api.example.com/path",
expected: "https://api.example.com/path",
},
{
name: "empty URL",
input: "",
expected: "unknown",
},
{
name: "invalid URL",
input: "not-a-url",
expected: "relative_or_invalid",
},
{
name: "very long path",
input: "https://example.com/" + strings.Repeat("a", 150),
expected: "https://example.com/" + strings.Repeat("a", 99) + "...", // 99 + 1 ("/") = 100 chars
},
{
name: "root path",
input: "https://example.com",
expected: "https://example.com/",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := sanitizeURL(tc.input)
assert.Equal(t, tc.expected, result)
})
}
}
func TestSaneHttpClientMetrics(t *testing.T) {
// Create a test server that returns different status codes
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/success":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("success"))
case "/error":
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("error"))
case "/notfound":
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte("not found"))
default:
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("default"))
}
}))
defer server.Close()
// Create a SaneHttpClient
client := SaneHttpClient()
testCases := []struct {
name string
path string
expectedStatusCode int
expectsNon200 bool
}{
{
name: "successful request",
path: "/success",
expectedStatusCode: 200,
expectsNon200: false,
},
{
name: "server error request",
path: "/error",
expectedStatusCode: 500,
expectsNon200: true,
},
{
name: "not found request",
path: "/notfound",
expectedStatusCode: 404,
expectsNon200: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var requestURL string
if strings.HasPrefix(tc.path, "http") {
requestURL = tc.path
} else {
requestURL = server.URL + tc.path
}
// Get initial metric values
sanitizedURL := sanitizeURL(requestURL)
initialRequestsTotal := testutil.ToFloat64(httpRequestsTotal.WithLabelValues(sanitizedURL))
// Make the request
resp, err := client.Get(requestURL)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
// Check that request counter was incremented
requestsTotal := testutil.ToFloat64(httpRequestsTotal.WithLabelValues(sanitizedURL))
assert.Equal(t, initialRequestsTotal+1, requestsTotal)
})
}
}
func TestRetryableHttpClientMetrics(t *testing.T) {
// Create a test server that returns different status codes
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/success":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("success"))
case "/error":
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("error"))
case "/notfound":
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte("not found"))
default:
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("default"))
}
}))
defer server.Close()
// Create a RetryableHttpClient
client := RetryableHTTPClient()
testCases := []struct {
name string
path string
expectedStatusCode int
}{
{
name: "successful request",
path: "/success",
expectedStatusCode: 200,
},
{
name: "not found request",
path: "/notfound",
expectedStatusCode: 404,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var requestURL string
if strings.HasPrefix(tc.path, "http") {
requestURL = tc.path
} else {
requestURL = server.URL + tc.path
}
// Get initial metric values
sanitizedURL := sanitizeURL(requestURL)
initialRequestsTotal := testutil.ToFloat64(httpRequestsTotal.WithLabelValues(sanitizedURL))
// Make the request
resp, err := client.Get(requestURL)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
// Check that request counter was incremented
requestsTotal := testutil.ToFloat64(httpRequestsTotal.WithLabelValues(sanitizedURL))
assert.Equal(t, initialRequestsTotal+1, requestsTotal)
})
}
}
func TestInstrumentedTransport(t *testing.T) {
// Create a mock transport that we can control
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("test response"))
}))
defer server.Close()
// Create instrumented transport
transport := NewInstrumentedTransport(nil)
client := &http.Client{
Transport: transport,
Timeout: 5 * time.Second,
}
// Get initial metric value
sanitizedURL := sanitizeURL(server.URL)
initialCount := testutil.ToFloat64(httpRequestsTotal.WithLabelValues(sanitizedURL))
// Make a request
resp, err := client.Get(server.URL)
require.NoError(t, err)
defer resp.Body.Close()
// Verify the request was successful
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Verify metrics were recorded
finalCount := testutil.ToFloat64(httpRequestsTotal.WithLabelValues(sanitizedURL))
assert.Equal(t, initialCount+1, finalCount)
// Note: Testing histogram metrics is complex due to the way Prometheus handles them
// The main thing is that the request completed successfully and counters were incremented
}
================================================
FILE: pkg/common/metrics.go
================================================
package common
const (
// MetricsNamespace is the namespace for all metrics.
MetricsNamespace = "trufflehog"
// MetricsSubsystem is the subsystem for all metrics.
MetricsSubsystem = "scanner"
)
================================================
FILE: pkg/common/patterns.go
================================================
package common
import (
"fmt"
"regexp"
"strconv"
"strings"
)
const EmailPattern = `\b((?i)(?:[a-z0-9!#$%&'*+/=?^_\x60{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_\x60{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\]))\b`
const SubDomainPattern = `\b([A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?)\b`
const UUIDPattern = `\b([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b`
const UUIDPatternUpperCase = `\b([0-9A-Z]{8}-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{12})\b`
const RegexPattern = "0-9a-z"
const AlphaNumPattern = "0-9a-zA-Z"
const HexPattern = "0-9a-f"
type RegexState struct {
compiledRegex *regexp.Regexp
}
// Custom Regex functions
func BuildRegex(pattern string, specialChar string, length int) string {
return fmt.Sprintf(`\b([%s%s]{%s})\b`, pattern, specialChar, strconv.Itoa(length))
}
func BuildRegexJWT(firstRange, secondRange, thirdRange string) string {
if RangeValidation(firstRange) || RangeValidation(secondRange) || RangeValidation(thirdRange) {
panic("Min value should not be greater than or equal to max")
}
return fmt.Sprintf(`\b(ey[%s]{%s}.ey[%s-\/_]{%s}.[%s-\/_]{%s})\b`, AlphaNumPattern, firstRange, AlphaNumPattern, secondRange, AlphaNumPattern, thirdRange)
}
func RangeValidation(rangeInput string) bool {
range_split := strings.Split(rangeInput, ",")
range_min, _ := strconv.ParseInt(strings.TrimSpace(range_split[0]), 10, 0)
range_max, _ := strconv.ParseInt(strings.TrimSpace(range_split[1]), 10, 0)
return range_min >= range_max
}
func ToUpperCase(input string) string {
return strings.ToUpper(input)
}
func (r RegexState) Matches(data []byte) []string {
matches := r.compiledRegex.FindAllStringSubmatch(string(data), -1)
res := make([]string, 0, len(matches))
// trim off all white spaces and different quote types ('"") & some special characters (,;).
for i := range matches {
res = append(res, strings.Trim(strings.TrimSpace(matches[i][1]), `"' ),;`))
}
return res
}
// UsernameRegexCheck constructs an username usernameRegex pattern from a given pattern of excluded characters.
func UsernameRegexCheck(pattern string) RegexState {
raw := fmt.Sprintf(`(?im)(?:user|usr)\S{0,40}?[:=\s]{1,3}[ '"=]{0,1}([^:%+v]{4,40})\b`, pattern)
return RegexState{regexp.MustCompile(raw)}
}
// PasswordRegexCheck constructs an username usernameRegex pattern from a given pattern of excluded characters.
func PasswordRegexCheck(pattern string) RegexState {
raw := fmt.Sprintf(`(?im)(?:pass|password)\S{0,40}?[:=\s]{1,3}[ '"=]{0,1}([^:%+v]{4,40})`, pattern)
return RegexState{regexp.MustCompile(raw)}
}
================================================
FILE: pkg/common/patterns_test.go
================================================
package common
import (
"regexp"
"testing"
"github.com/stretchr/testify/assert"
)
const (
usernamePattern = `?()/\+=\s\n`
passwordPattern = `^<>;.*&|£\n\s`
usernameRegex = `(?im)(?:user|usr)\S{0,40}?[:=\s]{1,3}[ '"=]{0,1}([^:?()/\+=\s\n]{4,40})\b`
passwordRegex = `(?im)(?:pass|password)\S{0,40}?[:=\s]{1,3}[ '"=]{0,1}([^:^<>;.*&|£\n\s]{4,40})`
)
func TestEmailRegexCheck(t *testing.T) {
testEmails := `
// positive cases
standard email = john.doe@example.com
subdomain email = jane_doe123@sub.domain.co.us
organization email = alice.smith@test.org
test email = bob@test.name
with tag email = user.name+tag@domain.com
hyphen domain = info@my-site.net
service email = contact@web-service.io
underscore email = example_user@domain.info
department email = first.last@department.company.edu
alphanumeric email = user1234@domain.co
local server email = admin@local-server.local
dot email = test.email@my-email-service.xyz
special char email = special@characters.com
support email = support@customer-service.org
insenstive email = ADMIN@example.com
insenstive domain = ADMIN@COMPANY.COM
mix email = USER123xyz@local-Server.local
// negative cases
not an email = abc.123@z
looks like email = test@user <- no domain
random text = here's some information about local-user@edu user
`
expectedStr := []string{
"john.doe@example.com", "jane_doe123@sub.domain.co.us",
"alice.smith@test.org", "bob@test.name", "user.name+tag@domain.com",
"info@my-site.net", "contact@web-service.io", "example_user@domain.info",
"first.last@department.company.edu", "user1234@domain.co", "admin@local-server.local",
"test.email@my-email-service.xyz", "special@characters.com", "support@customer-service.org",
"ADMIN@example.com", "ADMIN@COMPANY.COM", "USER123xyz@local-Server.local",
}
emailRegex := regexp.MustCompile(EmailPattern)
emailMatches := emailRegex.FindAllString(testEmails, -1)
assert.Exactly(t, emailMatches, expectedStr)
}
func TestUsernameRegexCheck(t *testing.T) {
usernameRegexPat := UsernameRegexCheck(usernamePattern)
expectedRegexPattern := regexp.MustCompile(usernameRegex)
if usernameRegexPat.compiledRegex.String() != expectedRegexPattern.String() {
t.Errorf("\n got %v \n want %v", usernameRegexPat.compiledRegex, expectedRegexPattern)
}
testString := `username = "johnsmith123"
username='johnsmith123'
username:="johnsmith123"
username:="johnsmith123",
username:="johnsmith123";
username = johnsmith123
username=johnsmith123`
expectedStr := []string{
"johnsmith123",
"johnsmith123",
"johnsmith123",
"johnsmith123",
"johnsmith123",
"johnsmith123",
"johnsmith123",
}
usernameRegexMatches := usernameRegexPat.Matches([]byte(testString))
assert.Exactly(t, usernameRegexMatches, expectedStr)
}
func TestPasswordRegexCheck(t *testing.T) {
passwordRegexPat := PasswordRegexCheck(passwordPattern)
expectedRegexPattern := regexp.MustCompile(passwordRegex)
assert.Equal(t, passwordRegexPat.compiledRegex, expectedRegexPattern)
testString := `password = "johnsmith123$!"
password='johnsmith123$!'
password:="johnsmith123$!"
password:="johnsmith123$!",
password:="johnsmith123$!";
password:="johnsmi',th123$!";
password = johnsmith123$!
password=johnsmith123$!
PasswordAuthenticator(username, "johnsmith123$!")`
expectedStr := []string{
"johnsmith123$!",
"johnsmith123$!",
"johnsmith123$!",
"johnsmith123$!",
"johnsmith123$!",
"johnsmi',th123$!",
"johnsmith123$!",
"johnsmith123$!",
"johnsmith123$!",
}
passwordRegexMatches := passwordRegexPat.Matches([]byte(testString))
assert.Exactly(t, passwordRegexMatches, expectedStr)
}
================================================
FILE: pkg/common/recover.go
================================================
package common
import (
"fmt"
"os"
"runtime/debug"
"time"
"github.com/getsentry/sentry-go"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
// Recover handles panics and reports to Sentry.
func Recover(ctx context.Context) {
if err := recover(); err != nil {
panicStack := string(debug.Stack())
if eventID := sentry.CurrentHub().Recover(err); eventID != nil {
ctx.Logger().Info("panic captured", "event_id", *eventID)
}
ctx.Logger().Error(fmt.Errorf("panic"), panicStack,
"recover", err,
)
if !sentry.Flush(time.Second * 5) {
ctx.Logger().Info("sentry flush failed")
}
}
}
// RecoverWithHandler handles panics and reports to Sentry, then turns control
// over to a provided function. This permits extra reporting in the same scope
// without re-panicking, as recover() clears the state after it's called. Does
// NOT block to flush sentry report.
func RecoverWithHandler(ctx context.Context, callback func(error)) {
if err := recover(); err != nil {
panicStack := string(debug.Stack())
if eventID := sentry.CurrentHub().Recover(err); eventID != nil {
ctx.Logger().Info("panic captured", "event_id", *eventID)
}
ctx.Logger().Error(fmt.Errorf("panic"), panicStack,
"recover", err,
)
switch v := err.(type) {
case error:
callback(fmt.Errorf("panic: %w", v))
default:
callback(fmt.Errorf("panic: %v", v))
}
}
}
// RecoverWithExit handles panics and reports to Sentry before exiting.
func RecoverWithExit(ctx context.Context) {
if err := recover(); err != nil {
panicStack := string(debug.Stack())
if eventID := sentry.CurrentHub().Recover(err); eventID != nil {
ctx.Logger().Info("panic captured", "event_id", *eventID)
}
ctx.Logger().Error(fmt.Errorf("panic"), "recovered from panic before exiting",
"stack-trace", panicStack,
"recover", err,
)
if !sentry.Flush(time.Second * 5) {
ctx.Logger().Info("sentry flush failed")
}
os.Exit(1)
}
}
================================================
FILE: pkg/common/secrets.go
================================================
package common
import (
"context"
"fmt"
"os"
"time"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
"github.com/joho/godotenv"
"github.com/pkg/errors"
)
type Secret struct{ kv map[string]string }
func (s *Secret) MustGetField(name string) string {
val, ok := s.kv[name]
if !ok {
panic(errors.Errorf("field %s not found", name))
}
return val
}
func GetSecretFromEnv(filename string) (secret *Secret, err error) {
data, err := godotenv.Read(filename)
if err != nil {
return nil, err
}
return &Secret{kv: data}, nil
}
func GetTestSecret(ctx context.Context) (secret *Secret, err error) {
filename := os.Getenv("TEST_SECRET_FILE")
if len(filename) > 0 {
return GetSecretFromEnv(filename)
}
return GetSecret(ctx, "trufflehog-testing", "test")
}
func GetSecret(ctx context.Context, gcpProject, name string) (secret *Secret, err error) {
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
filename := os.Getenv("TEST_SECRET_FILE")
if len(filename) > 0 {
return GetSecretFromEnv(filename)
}
parent := fmt.Sprintf("projects/%s/secrets/%s/versions/latest", gcpProject, name)
client, err := secretmanager.NewClient(ctx)
if err != nil {
return nil, errors.Errorf("failed to create secretmanager client: %v", err)
}
defer client.Close()
req := &secretmanagerpb.AccessSecretVersionRequest{
Name: parent,
}
result, err := client.AccessSecretVersion(ctx, req)
if err != nil {
return nil, errors.Errorf("failed to access secret version: %v", err)
}
data, err := godotenv.Unmarshal(string(result.Payload.Data))
if err != nil {
return nil, err
}
return &Secret{kv: data}, nil
}
================================================
FILE: pkg/common/utils.go
================================================
package common
import (
"bufio"
"crypto/rand"
"io"
"math/big"
mrand "math/rand"
"strings"
)
func AddStringSliceItem(item string, slice *[]string) {
for _, i := range *slice {
if i == item {
return
}
}
*slice = append(*slice, item)
}
func RemoveStringSliceItem(item string, slice *[]string) {
for i, listItem := range *slice {
if item == listItem {
(*slice)[i] = (*slice)[len(*slice)-1]
*slice = (*slice)[:len(*slice)-1]
}
}
}
func ResponseContainsSubstring(reader io.ReadCloser, target string) (bool, error) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
if strings.Contains(scanner.Text(), target) {
return true, nil
}
}
if err := scanner.Err(); err != nil {
return false, err
}
return false, nil
}
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
// RandomID returns a random string of the given length.
func RandomID(length int) string {
b := make([]rune, length)
for i := range b {
randInt, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
b[i] = letters[randInt.Int64()]
}
return string(b)
}
// SliceContainsString searches a slice to determine if it contains a specified string.
// Returns the index of the first match in the slice.
func SliceContainsString(origTargetString string, stringSlice []string, ignoreCase bool) (bool, string, int) {
targetString := origTargetString
if ignoreCase {
targetString = strings.ToLower(origTargetString)
}
for i, origStringFromSlice := range stringSlice {
stringFromSlice := origStringFromSlice
if ignoreCase {
stringFromSlice = strings.ToLower(origStringFromSlice)
}
if targetString == stringFromSlice {
return true, targetString, i
}
}
return false, "", 0
}
// GoFakeIt Password generator does not guarantee inclusion of characters.
// Using a custom random password generator with guaranteed inclusions (atleast) of lower, upper, numeric and special characters
func GenerateRandomPassword(lower, upper, numeric, special bool, length int) string {
if length < 1 {
return ""
}
var password []rune
var required []rune
var allowed []rune
lowerChars := []rune("abcdefghijklmnopqrstuvwxyz")
upperChars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
specialChars := []rune("!@#$%^&*()-_=+[]{}|;:',.<>?/")
numberChars := []rune("0123456789")
// Ensure inclusion from each requested category
if lower {
rand, _ := rand.Int(rand.Reader, big.NewInt(int64(len(lowerChars))))
ch := lowerChars[rand.Int64()]
required = append(required, ch)
allowed = append(allowed, lowerChars...)
}
if upper {
rand, _ := rand.Int(rand.Reader, big.NewInt(int64(len(upperChars))))
ch := upperChars[rand.Int64()]
required = append(required, ch)
allowed = append(allowed, upperChars...)
}
if numeric {
rand, _ := rand.Int(rand.Reader, big.NewInt(int64(len(numberChars))))
ch := numberChars[rand.Int64()]
required = append(required, ch)
allowed = append(allowed, numberChars...)
}
if special {
rand, _ := rand.Int(rand.Reader, big.NewInt(int64(len(specialChars))))
ch := specialChars[rand.Int64()]
required = append(required, ch)
allowed = append(allowed, specialChars...)
}
if len(allowed) == 0 {
return "" // No character sets enabled
}
// Fill the rest of the password
for i := 0; i < length-len(required); i++ {
rand, _ := rand.Int(rand.Reader, big.NewInt(int64(len(allowed))))
ch := allowed[rand.Int64()]
password = append(password, ch)
}
// Combine required and random characters, then shuffle
password = append(password, required...)
mrand.Shuffle(len(password), func(i, j int) {
password[i], password[j] = password[j], password[i]
})
return string(password)
}
================================================
FILE: pkg/common/utils_test.go
================================================
package common
import (
"io"
"reflect"
"strings"
"testing"
"unicode"
)
func TestAddItem(t *testing.T) {
type Case struct {
Slice []string
Modifier []string
Expected []string
}
tests := map[string]Case{
"newItem": {
Slice: []string{"a", "b", "c"},
Modifier: []string{"d"},
Expected: []string{"a", "b", "c", "d"},
},
"newDuplicate": {
Slice: []string{"a", "b", "c"},
Modifier: []string{"c"},
Expected: []string{"a", "b", "c"},
},
}
for name, test := range tests {
for _, item := range test.Modifier {
AddStringSliceItem(item, &test.Slice)
}
if !reflect.DeepEqual(test.Slice, test.Expected) {
t.Errorf("%s: expected:%v, got:%v", name, test.Expected, test.Slice)
}
}
}
func TestRemoveItem(t *testing.T) {
type Case struct {
Slice []string
Modifier []string
Expected []string
}
tests := map[string]Case{
"existingItemEnd": {
Slice: []string{"a", "b", "c"},
Modifier: []string{"c"},
Expected: []string{"a", "b"},
},
"existingItemMiddle": {
Slice: []string{"a", "b", "c"},
Modifier: []string{"b"},
Expected: []string{"a", "c"},
},
"existingItemBeginning": {
Slice: []string{"a", "b", "c"},
Modifier: []string{"a"},
Expected: []string{"c", "b"},
},
"nonExistingItem": {
Slice: []string{"a", "b", "c"},
Modifier: []string{"d"},
Expected: []string{"a", "b", "c"},
},
}
for name, test := range tests {
for _, item := range test.Modifier {
RemoveStringSliceItem(item, &test.Slice)
}
if !reflect.DeepEqual(test.Slice, test.Expected) {
t.Errorf("%s: expected:%v, got:%v", name, test.Expected, test.Slice)
}
}
}
// Test ParseResponseForKeywords with a reader that contains the keyword and a reader that doesn't.
func TestParseResponseForKeywords(t *testing.T) {
testCases := []struct {
name string
input string
keyword string
expected bool
}{
{
name: "Should find keyword",
input: "ey: abc",
keyword: "ey",
expected: true,
},
{
name: "Should not find keyword",
input: "fake response",
keyword: "ey",
expected: false,
},
{
name: "Empty string",
input: "",
keyword: "ey",
expected: false,
},
{
name: "Keyword at end",
input: "abc ey",
keyword: "ey",
expected: true,
},
{
name: "Keyword at start",
input: "ey abc",
keyword: "ey",
expected: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
testReader := strings.NewReader(tc.input)
testReadCloser := io.NopCloser(testReader)
found, err := ResponseContainsSubstring(testReadCloser, tc.keyword)
if err != nil {
t.Errorf("Error: %v", err)
}
if found != tc.expected {
t.Errorf("Expected %v, got %v", tc.expected, found)
}
})
}
}
func TestSliceContainsString(t *testing.T) {
testCases := []struct {
name string
slice []string
target string
expectedBool bool
expectedString string
expectedIndex int
ignoreCase bool
}{
{
name: "matching case, target exists",
slice: []string{"one", "two", "three"},
target: "two",
expectedBool: true,
expectedString: "two",
expectedIndex: 1,
ignoreCase: false,
},
{
name: "non-matching case, target exists, ignore case",
slice: []string{"one", "two", "three"},
target: "Two",
expectedBool: true,
expectedString: "two",
expectedIndex: 1,
ignoreCase: true,
},
{
name: "non-matching case, target in wrong case, case respected",
slice: []string{"one", "two", "three"},
target: "Two",
expectedBool: false,
expectedString: "",
expectedIndex: 0,
ignoreCase: false,
},
{
name: "target not in slice",
slice: []string{"one", "two", "three"},
target: "four",
expectedBool: false,
expectedString: "",
expectedIndex: 0,
ignoreCase: false,
},
}
for _, testCase := range testCases {
resultBool, resultString, resultIndex := SliceContainsString(testCase.target, testCase.slice, testCase.ignoreCase)
if resultBool != testCase.expectedBool {
t.Errorf("%s: bool values do not match. Got: %t, expected: %t", testCase.name, resultBool, testCase.expectedBool)
}
if resultString != testCase.expectedString {
t.Errorf("%s: string values do not match. Got: %s, expected: %s", testCase.name, resultString, testCase.expectedString)
}
if resultIndex != testCase.expectedIndex {
t.Errorf("%s: index values do not match. Got: %d, expected: %d", testCase.name, resultIndex, testCase.expectedIndex)
}
}
}
func TestGenerateRandomPassword_Length(t *testing.T) {
pass := GenerateRandomPassword(true, true, true, true, 16)
if len(pass) != 16 {
t.Errorf("expected length 16, got %d", len(pass))
}
}
func TestGenerateRandomPassword_Empty(t *testing.T) {
pass := GenerateRandomPassword(false, false, false, false, 10)
if pass != "" {
t.Errorf("expected empty string, got %q", pass)
}
}
func TestGenerateRandomPassword_RequiredSets(t *testing.T) {
tests := []struct {
name string
lower bool
upper bool
numeric bool
special bool
}{
{"lower only", true, false, false, false},
{"upper only", false, true, false, false},
{"numeric only", false, false, true, false},
{"special only", false, false, false, true},
{"all", true, true, true, true},
{"lower+upper", true, true, false, false},
{"lower+numeric", true, false, true, false},
{"upper+special", false, true, false, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
pass := GenerateRandomPassword(tc.lower, tc.upper, tc.numeric, tc.special, 12)
if len(pass) != 12 {
t.Errorf("expected length 12, got %d", len(pass))
}
if tc.lower && !contains(pass, unicode.IsLower) {
t.Errorf("expected at least one lowercase letter")
}
if tc.upper && !contains(pass, unicode.IsUpper) {
t.Errorf("expected at least one uppercase letter")
}
if tc.numeric && !contains(pass, unicode.IsDigit) {
t.Errorf("expected at least one digit")
}
if tc.special && !containsSpecial(pass) {
t.Errorf("expected at least one special character")
}
})
}
}
func TestGenerateRandomPassword_ShortLength(t *testing.T) {
pass := GenerateRandomPassword(true, true, true, true, 0)
if pass != "" {
t.Errorf("expected empty string for length 0, got %q", pass)
}
}
func contains(s string, fn func(rune) bool) bool {
for _, r := range s {
if fn(r) {
return true
}
}
return false
}
func containsSpecial(s string) bool {
specials := "!@#$%^&*()-_=+[]{}|;:',.<>?/"
for _, r := range s {
for _, sr := range specials {
if r == sr {
return true
}
}
}
return false
}
================================================
FILE: pkg/common/vars.go
================================================
package common
import (
"path/filepath"
"strings"
)
var (
KB, MB, GB, TB, PB = 1e3, 1e6, 1e9, 1e12, 1e15
ignoredExtensions = map[string]struct{}{
// images
"apng": {},
"avif": {},
"avifs": {},
"bmp": {},
"dia": {}, // Open-source Visio clone
"gif": {},
"icns": {}, // Apple icon image file
"ico": {}, // Icon file
"jpg": {},
"jpeg": {},
"jxl": {},
"png": {},
"svg": {},
"svgz": {}, // Compressed Scalable Vector Graphics file
"tga": {},
"tif": {},
"tiff": {},
"vsdx": {}, // Microsoft Visio drawing file
"vsix": {}, // Visual Studio extension file
// audio
"fev": {}, // video game audio
"fsb": {},
"m2a": {},
"m4a": {},
"mp2": {},
"mp3": {},
"snag": {},
// video
"264": {},
"3gp": {},
"avi": {},
"flac": {},
"flv": {},
"hdv": {},
"m4p": {},
"mov": {},
"mp4": {},
"mpg": {},
"mpeg": {},
"ogg": {},
"qt": {},
"swf": {},
"vob": {},
"wav": {},
"webm": {},
"webp": {},
"wmv": {},
// documents
"pdf": {},
"psd": {},
// fonts
"eot": {}, // Embedded OpenType font
"fnt": {}, // Windows font file
"fon": {}, // Generic font file
"otf": {}, // OpenType font
"ttf": {}, // TrueType font
"woff": {}, // Web Open Font Format
"woff2": {}, // Web Open Font Format 2
// misc
"glb": {}, // 3d models (binary)
"gltf": {}, // 3d models (JSON/ASCII)
}
binaryExtensions = map[string]struct{}{
// binaries
// These can theoretically contain secrets, but need decoding for users to make sense of them, and we don't have
// any such decoders right now.
"class": {}, // Java bytecode class file
"dll": {}, // Dynamic Link Library, Windows
"jdo": {}, // Java Data Object, Java serialization format
"jks": {}, // Java Key Store, Java keystore format
"ser": {}, // Java serialization format
"idx": {}, // Index file, often binary
"hprof": {}, // Java heap dump format
"exe": {}, // Executable, Windows
"bin": {}, // Binary, often used for compiled source code
"so": {}, // Shared object, Unix/Linux
"o": {}, // Object file from compilation/ intermediate object file
"a": {}, // Static library, Unix/Linux
"dylib": {}, // Dynamic library, macOS
"lib": {}, // Library, Unix/Linux
"obj": {}, // Object file, typically from compiled source code
"pdb": {}, // Program Database, Microsoft Visual Studio debugging format
"dat": {}, // Generic data file, often binary but not always
"elf": {}, // Executable and Linkable Format, common in Unix/Linux
"dmg": {}, // Disk Image for macOS
"iso": {}, // ISO image (optical disk image)
"img": {}, // Disk image files
"out": {}, // Common output file from compiled executable in Unix/Linux
"com": {}, // DOS command file, executable
"sys": {}, // Windows system file, often a driver
"vxd": {}, // Virtual device driver in Windows
"sfx": {}, // Self-extracting archive
"bundle": {}, // Mac OS X application bundle
"pyo": {}, // Compiled Python file
"pyc": {}, // Compiled Python file
"sym": {}, // Symbolic link, Unix/Linux
"rlib": {}, // Rust library
"pth": {}, // Pytorch serialized model
"pbix": {}, // Power BI report file
"pbit": {}, // Power BI template file
}
)
// SkipFile returns true if the file extension is in the ignoredExtensions list.
func SkipFile(filename string) bool {
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
_, ok := ignoredExtensions[ext]
return ok
}
// IsBinary returns true if the file extension is in the binaryExtensions list.
func IsBinary(filename string) bool {
_, ok := binaryExtensions[strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))]
return ok
}
================================================
FILE: pkg/common/vars_test.go
================================================
package common
import (
"strings"
"testing"
)
func TestSkipFile(t *testing.T) {
type testCase struct {
file string
want bool
}
// Add a test case for each ignored extension.
testCases := make([]testCase, 0, (len(ignoredExtensions)+1)*2)
for ext := range ignoredExtensions {
testCases = append(testCases, testCase{
file: "test." + ext,
want: true,
})
testCases = append(testCases, testCase{
file: "test." + strings.ToUpper(ext),
want: true,
})
}
// Add a test case for a file that should not be skipped.
testCases = append(testCases, testCase{file: "test.txt", want: false})
for _, tt := range testCases {
t.Run(tt.file, func(t *testing.T) {
if got := SkipFile(tt.file); got != tt.want {
t.Errorf("SkipFile(%v) got %v, want %v", tt.file, got, tt.want)
}
})
}
}
func BenchmarkSkipFile(b *testing.B) {
for i := 0; i < b.N; i++ {
SkipFile("test.mp4")
}
}
func TestIsBinary(t *testing.T) {
type testCase struct {
file string
want bool
}
// Add a test case for each binary extension.
testCases := make([]testCase, 0, len(binaryExtensions)+1)
for ext := range binaryExtensions {
testCases = append(testCases, testCase{
file: "test." + ext,
want: true,
})
}
// Add a test case for a file that should not be skipped.
testCases = append(testCases, testCase{file: "test.txt", want: false})
for _, tt := range testCases {
t.Run(tt.file, func(t *testing.T) {
if got := IsBinary(tt.file); got != tt.want {
t.Errorf("IsBinary(%v) got %v, want %v", tt.file, got, tt.want)
}
})
}
}
func BenchmarkIsBinary(b *testing.B) {
for i := 0; i < b.N; i++ {
IsBinary("test.exe")
}
}
================================================
FILE: pkg/config/config.go
================================================
package config
import (
"fmt"
"os"
"github.com/trufflesecurity/trufflehog/v3/pkg/custom_detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/configpb"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
"github.com/trufflesecurity/trufflehog/v3/pkg/protoyaml"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/docker"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/filesystem"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/gcs"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/git"
"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/postman"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/s3"
)
// Config holds user supplied configuration.
type Config struct {
Sources []sources.ConfiguredSource
Detectors []detectors.Detector
}
// Read parses a given filename into a Config.
func Read(filename string) (*Config, error) {
input, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return NewYAML(input)
}
// NewYAML parses the given YAML data into a Config.
func NewYAML(input []byte) (*Config, error) {
var inputYAML configpb.Config
// Parse the raw YAML into a structure.
if err := protoyaml.UnmarshalStrict(input, &inputYAML); err != nil {
return nil, err
}
// Convert to detectors.
var detectorConfigs []detectors.Detector
for _, detectorConfig := range inputYAML.Detectors {
detector, err := custom_detectors.NewWebhookCustomRegex(detectorConfig)
if err != nil {
return nil, err
}
detectorConfigs = append(detectorConfigs, detector)
}
// Convert to configured sources.
var sourceConfigs []sources.ConfiguredSource
for _, pbSource := range inputYAML.Sources {
s, err := instantiateSourceFromType(pbSource.GetType())
if err != nil {
return nil, err
}
src := sources.NewConfiguredSource(s, pbSource)
sourceConfigs = append(sourceConfigs, src)
}
return &Config{
Detectors: detectorConfigs,
Sources: sourceConfigs,
}, nil
}
// instantiateSourceFromType creates a concrete implementation of
// sources.Source for the provided type.
func instantiateSourceFromType(sourceType string) (sources.Source, error) {
var source sources.Source
switch sourceType {
case sourcespb.SourceType_SOURCE_TYPE_GIT.String():
source = new(git.Source)
case sourcespb.SourceType_SOURCE_TYPE_GITHUB.String():
source = new(github.Source)
case sourcespb.SourceType_SOURCE_TYPE_GITHUB_UNAUTHENTICATED_ORG.String():
source = new(github.Source)
case sourcespb.SourceType_SOURCE_TYPE_PUBLIC_GIT.String():
source = new(git.Source)
case sourcespb.SourceType_SOURCE_TYPE_GITLAB.String():
source = new(gitlab.Source)
case sourcespb.SourceType_SOURCE_TYPE_POSTMAN.String():
source = new(postman.Source)
case sourcespb.SourceType_SOURCE_TYPE_S3.String():
source = new(s3.Source)
case sourcespb.SourceType_SOURCE_TYPE_S3_UNAUTHED.String():
source = new(s3.Source)
case sourcespb.SourceType_SOURCE_TYPE_FILESYSTEM.String():
source = new(filesystem.Source)
case sourcespb.SourceType_SOURCE_TYPE_JENKINS.String():
source = new(jenkins.Source)
case sourcespb.SourceType_SOURCE_TYPE_GCS.String():
source = new(gcs.Source)
case sourcespb.SourceType_SOURCE_TYPE_GCS_UNAUTHED.String():
source = new(gcs.Source)
case sourcespb.SourceType_SOURCE_TYPE_DOCKER.String():
source = new(docker.Source)
default:
return nil, fmt.Errorf("got unexpected source type: %q", sourceType)
}
return source, nil
}
================================================
FILE: pkg/config/detectors.go
================================================
package config
import (
"fmt"
"net/url"
"sort"
"strconv"
"strings"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
dpb "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
var (
specialGroups = map[string][]DetectorID{
"all": allDetectors(),
}
detectorTypeValue = make(map[string]dpb.DetectorType, len(dpb.DetectorType_value))
validDetectors = make(map[dpb.DetectorType]struct{}, len(dpb.DetectorType_value))
maxDetectorType dpb.DetectorType
)
// Setup package local global variables.
func init() {
for k, v := range dpb.DetectorType_value {
dt := dpb.DetectorType(v)
detectorTypeValue[strings.ToLower(k)] = dt
validDetectors[dt] = struct{}{}
if dt > maxDetectorType {
maxDetectorType = dt
}
}
}
// DetectorID identifies a detector type and version. This struct is used as a
// way for users to identify detectors, whether unique or not. A DetectorID
// with Version = 0 indicates all possible versions of a detector.
type DetectorID struct {
ID dpb.DetectorType
Version int
}
// GetDetectorID extracts the DetectorID from a Detector.
func GetDetectorID(d detectors.Detector) DetectorID {
var version int
if v, ok := d.(detectors.Versioner); ok {
version = v.Version()
}
return DetectorID{
ID: d.Type(),
Version: version,
}
}
// ParseDetectors parses user supplied string into a list of detectors types.
// "all" will return the list of all available detectors. The input is comma
// separated and may use the case-insensitive detector name defined in the
// protobuf, or the protobuf enum number. A range may be used as well in the
// form "start-end". Order is preserved and duplicates are ignored.
func ParseDetectors(input string) ([]DetectorID, error) {
var output []DetectorID
seenDetector := map[DetectorID]struct{}{}
for _, item := range strings.Split(input, ",") {
item = strings.TrimSpace(item)
if item == "" {
continue
}
allDetectors, ok := specialGroups[strings.ToLower(item)]
if !ok {
var err error
allDetectors, err = asRange(item)
if err != nil {
return nil, err
}
}
for _, d := range allDetectors {
if _, ok := seenDetector[d]; ok {
continue
}
seenDetector[d] = struct{}{}
output = append(output, d)
}
}
return output, nil
}
// ParseDetector parses a user supplied string into a single DetectorID. Input
// is case-insensitive and either the detector name or ID may be used.
func ParseDetector(input string) (DetectorID, error) {
return asDetectorID(strings.TrimSpace(input))
}
// ParseVerifierEndpoints parses a map of user supplied verifier URLs. The
// input keys are detector IDs and the values are a comma separated list of
// URLs. The URLs are validated as HTTPS endpoints.
func ParseVerifierEndpoints(verifierURLs map[string]string) (map[DetectorID][]string, error) {
verifiers := make(map[DetectorID][]string, len(verifierURLs))
for detectorID, urls := range verifierURLs {
key, err := ParseDetector(detectorID)
if err != nil {
return nil, fmt.Errorf("invalid detector ID for verifier: %w", err)
}
verifierURLs := strings.Split(urls, ",")
for i, rawEndpoint := range verifierURLs {
rawEndpoint := strings.TrimSpace(rawEndpoint)
verifierURLs[i] = rawEndpoint
if endpoint, err := url.Parse(rawEndpoint); err != nil {
return nil, fmt.Errorf("invalid verifier url %q: %w", rawEndpoint, err)
} else if endpoint.Scheme != "https" {
return nil, fmt.Errorf("verifier url must be https: %q", rawEndpoint)
}
}
verifiers[key] = append(verifiers[key], verifierURLs...)
}
return verifiers, nil
}
func (id DetectorID) String() string {
name := dpb.DetectorType_name[int32(id.ID)]
if name == "" {
name = ""
}
if id.Version == 0 {
return name
}
return fmt.Sprintf("%s.v%d", name, id.Version)
}
// allDetectors returns an ordered slice of all detector types.
func allDetectors() []DetectorID {
all := make([]DetectorID, 0, len(dpb.DetectorType_name))
for id := range dpb.DetectorType_name {
all = append(all, DetectorID{ID: dpb.DetectorType(id)})
}
sort.Slice(all, func(i, j int) bool { return all[i].ID < all[j].ID })
return all
}
// asRange converts a single input into a slice of detector types. If the input
// is not in range format, a slice of length 1 is returned. Unbounded ranges
// are allowed.
func asRange(input string) ([]DetectorID, error) {
// Check if it's a single detector type.
dt, err := asDetectorID(input)
if err == nil {
return []DetectorID{dt}, nil
}
// Check if it's a range; if not return the error from above.
start, end, found := strings.Cut(input, "-")
if !found {
return nil, err
}
start, end = strings.TrimSpace(start), strings.TrimSpace(end)
// Convert the range start and end to a DetectorType.
dtStart, err := asDetectorID(start)
if err != nil {
return nil, err
}
dtEnd, err := asDetectorID(end)
// If end is empty it's an unbounded range.
if err != nil && end != "" {
return nil, err
}
if end == "" {
dtEnd.ID = maxDetectorType
}
// Ensure these ranges don't have versions.
if dtEnd.Version != 0 || dtStart.Version != 0 {
return nil, fmt.Errorf("versions within ranges are not supported: %s", input)
}
step := dpb.DetectorType(1)
if dtStart.ID > dtEnd.ID {
step = -1
}
var output []DetectorID
for dt := dtStart.ID; dt != dtEnd.ID; dt += step {
if _, ok := validDetectors[dt]; !ok {
continue
}
output = append(output, DetectorID{ID: dt})
}
return append(output, dtEnd), nil
}
// asDetectorID converts the case-insensitive input into a DetectorID. Name or
// ID may be used. Input is expected to be already trimmed of whitespace.
func asDetectorID(input string) (DetectorID, error) {
if input == "" {
return DetectorID{}, fmt.Errorf("empty detector")
}
var detectorID DetectorID
// Separate the version if there is one.
if detector, version, hasVersion := strings.Cut(input, "."); hasVersion {
parsedVersion, err := parseVersion(version)
if err != nil {
return DetectorID{}, fmt.Errorf("invalid version for input: %q error: %w", input, err)
}
detectorID.Version = parsedVersion
// Because there was a version, the detector type input is the part before the '.'
input = detector
}
// Check if it's a named detector.
if dt, ok := detectorTypeValue[strings.ToLower(input)]; ok {
detectorID.ID = dt
return detectorID, nil
}
// Check if it's a detector ID.
if i, err := strconv.ParseInt(input, 10, 32); err == nil {
dt := dpb.DetectorType(i)
if _, ok := validDetectors[dt]; !ok {
return DetectorID{}, fmt.Errorf("invalid detector ID: %s", input)
}
detectorID.ID = dt
return detectorID, nil
}
return DetectorID{}, fmt.Errorf("unrecognized detector type: %s", input)
}
func parseVersion(v string) (int, error) {
if !strings.HasPrefix(strings.ToLower(v), "v") {
return 0, fmt.Errorf("version must start with 'v'")
}
version := strings.TrimLeft(v, "vV")
return strconv.Atoi(version)
}
================================================
FILE: pkg/config/detectors_test.go
================================================
package config
import (
"testing"
"github.com/stretchr/testify/assert"
dpb "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDetectorParsing(t *testing.T) {
tests := map[string]struct {
input string
expected []DetectorID
}{
"all": {"AlL", allDetectors()},
"trailing range": {"0-", allDetectors()},
"all after 1": {"1-", allDetectors()[1:]},
"named and valid range": {"aWs,8-9", []DetectorID{{ID: dpb.DetectorType_AWS}, {ID: dpb.DetectorType_Github}, {ID: dpb.DetectorType_Gitlab}}},
"duplicate order preserved": {"9, 8, 9", []DetectorID{{ID: 9}, {ID: 8}}},
"named range": {"github - gitlab", []DetectorID{{ID: dpb.DetectorType_Github}, {ID: dpb.DetectorType_Gitlab}}},
"range preserved": {"8-9, 7-10", []DetectorID{{ID: 8}, {ID: 9}, {ID: 7}, {ID: 10}}},
"reverse range": {"9-8", []DetectorID{{ID: 9}, {ID: 8}}},
"range preserved with all": {"10-,all", append(allDetectors()[10:], allDetectors()[:10]...)},
"empty list item": {"8, ,9", []DetectorID{{ID: 8}, {ID: 9}}},
"invalid end range": {"0-1337", nil},
"invalid name": {"foo", nil},
"negative": {"-1", nil},
"github.v1": {"github.v1", []DetectorID{{ID: dpb.DetectorType_Github, Version: 1}}},
"gitlab.v100": {"gitlab.v100", []DetectorID{{ID: dpb.DetectorType_Gitlab, Version: 100}}},
"range with versions": {"github.v2 - gitlab.v1", nil},
"invalid version no v": {"gitlab.2", nil},
"invalid version no number": {"gitlab.github", nil},
"capital V is fine": {"GiTlAb.V2", []DetectorID{{ID: dpb.DetectorType_Gitlab, Version: 2}}},
"id number with version": {"8.v2", []DetectorID{{ID: 8, Version: 2}}},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
got, gotErr := ParseDetectors(tt.input)
if tt.expected == nil {
assert.Error(t, gotErr)
return
}
assert.Equal(t, tt.expected, got)
})
}
}
================================================
FILE: pkg/context/context.go
================================================
package context
import (
"context"
"os"
"time"
"github.com/go-logr/logr"
"github.com/trufflesecurity/trufflehog/v3/pkg/log"
)
var (
// defaultLogger can be set via SetDefaultLogger.
// It is initialized to write to stderr. To disable, you can call
// SetDefaultLogger with logr.Discard().
defaultLogger logr.Logger
)
// logEntryKey is used to store a reference to the logger in the
// context.Context key/value bag. This is used for regaining the logger in case
// the context is converted into a different type.
const logEntryKey logEntryKeyT = 0
type logEntryKeyT int
func init() {
defaultLogger, _ = log.New("context", log.WithConsoleSink(os.Stderr))
}
// Context wraps context.Context and includes an additional Logger() method.
type Context interface {
context.Context
Logger() logr.Logger
}
// CancelFunc and CancelCauseFunc are type aliases to allow use as if they are
// the same types as the standard library variants.
type CancelFunc = context.CancelFunc
type CancelCauseFunc = context.CancelCauseFunc
// logCtx implements Context.
type logCtx struct {
// Embed context.Context to get all methods for free.
context.Context
log logr.Logger
}
// Logger returns a structured logger.
func (l logCtx) Logger() logr.Logger {
return l.log
}
// Background returns context.Background with a default logger.
func Background() Context {
return logCtx{
log: defaultLogger,
Context: context.Background(),
}
}
// TODO returns context.TODO with a default logger.
func TODO() Context {
return logCtx{
log: defaultLogger,
Context: context.TODO(),
}
}
// WithCancel returns context.WithCancel with the log object propagated.
func WithCancel(parent Context) (Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(parent)
lCtx := logCtx{
log: parent.Logger(),
Context: ctx,
}
return lCtx, cancel
}
// WithCancelCause returns context.WithCancelCause with the log object propagated.
func WithCancelCause(parent Context) (Context, context.CancelCauseFunc) {
ctx, cancel := context.WithCancelCause(parent)
lCtx := logCtx{
log: parent.Logger(),
Context: ctx,
}
return lCtx, cancel
}
// WithDeadline returns context.WithDeadline with the log object propagated and
// the deadline added to the structured log values.
func WithDeadline(parent Context, d time.Time) (Context, context.CancelFunc) {
ctx, cancel := context.WithDeadline(parent, d)
lCtx := logCtx{
log: parent.Logger().WithValues("deadline", d),
Context: ctx,
}
return lCtx, cancel
}
// WithDeadlineCause returns context.WithDeadlineCause with the log object
// propagated and the deadline added to the structured log values.
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, context.CancelFunc) {
ctx, cancel := context.WithDeadlineCause(parent, d, cause)
lCtx := logCtx{
log: parent.Logger().WithValues("deadline", d),
Context: ctx,
}
return lCtx, cancel
}
// WithTimeout returns context.WithTimeout with the log object propagated and
// the timeout added to the structured log values.
func WithTimeout(parent Context, timeout time.Duration) (Context, context.CancelFunc) {
ctx, cancel := context.WithTimeout(parent, timeout)
lCtx := logCtx{
log: parent.Logger().WithValues("timeout", timeout),
Context: ctx,
}
return lCtx, cancel
}
// WithTimeoutCause returns context.WithTimeoutCause with the log object
// propagated and the timeout added to the structured log values.
func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, context.CancelFunc) {
ctx, cancel := context.WithTimeoutCause(parent, timeout, cause)
lCtx := logCtx{
log: parent.Logger().WithValues("timeout", timeout),
Context: ctx,
}
return lCtx, cancel
}
// Cause returns the context.Cause of the context.
func Cause(ctx context.Context) error {
return context.Cause(ctx)
}
// WithValue returns context.WithValue with the log object propagated and
// the value added to the structured log values (if the key is a string).
func WithValue(parent Context, key, val any) Context {
logger := parent.Logger()
parentCtx := context.WithValue(parent, key, val)
if k, ok := key.(string); ok {
logger = logger.WithValues(k, val)
parentCtx = context.WithValue(parentCtx, logEntryKey, logger)
}
return logCtx{
log: logger,
Context: parentCtx,
}
}
// WithValues returns context.WithValue with the log object propagated and
// the values added to the structured log values (if the key is a string).
func WithValues(parent Context, keyAndVals ...any) Context {
ctx := parent
for i := 0; i < len(keyAndVals)-1; i += 2 {
ctx = WithValue(ctx, keyAndVals[i], keyAndVals[i+1])
}
return ctx
}
// WithLogger converts a context.Context into a Context by adding a logger.
func WithLogger(parent context.Context, logger logr.Logger) Context {
return logCtx{
log: logger,
Context: context.WithValue(parent, logEntryKey, logger),
}
}
// AddLogger converts a context.Context into a Context. If the underlying type
// is already a Context, that will be returned, otherwise a default logger will
// be added.
func AddLogger(parent context.Context) Context {
// If the context.Context is already a Context, return that.
if loggerCtx, ok := parent.(Context); ok {
return loggerCtx
}
// If the logger exists in the grab bag (and is the correct type),
// return that.
if logEntryVal := parent.Value(logEntryKey); logEntryVal != nil {
if logger, ok := logEntryVal.(logr.Logger); ok {
return WithLogger(parent, logger)
}
}
// Otherwise, add the default logger.
return WithLogger(parent, defaultLogger)
}
// SetDefaultLogger sets the package-level global default logger that will be
// used for Background and TODO contexts. On startup, the default logger will
// be configured to output logs to stderr. Use logr.Discard() to disable all
// logs from Contexts.
func SetDefaultLogger(l logr.Logger) {
defaultLogger = l
}
================================================
FILE: pkg/context/context_test.go
================================================
package context
import (
"bytes"
"context"
"fmt"
"strings"
"testing"
"time"
"github.com/go-logr/logr"
"github.com/go-logr/zapr"
"github.com/stretchr/testify/assert"
"github.com/trufflesecurity/trufflehog/v3/pkg/log"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest"
)
// testLogger is a helper function to create a logger with a closure callback.
func testLogger(t *testing.T, f func(zapcore.Entry)) logr.Logger {
return zapr.NewLogger(zaptest.NewLogger(t,
zaptest.WrapOptions(zap.Hooks(func(e zapcore.Entry) error {
f(e)
return nil
}))))
}
// infoCounterContext is a helper function to create a Context that will count
// the number of Info messages logged.
func infoCounterContext(t *testing.T) (Context, *int) {
var infoCount int
logger := testLogger(t, func(e zapcore.Entry) {
if e.Level == zap.InfoLevel {
infoCount++
}
})
return WithLogger(context.Background(), logger), &infoCount
}
func TestWithCancel(t *testing.T) {
parentCtx, infoCount := infoCounterContext(t)
ctx, cancel := WithCancel(parentCtx)
cancel()
assert.Equal(t, 0, *infoCount)
select {
case <-ctx.Done():
ctx.Logger().Info("yay")
case <-time.After(1 * time.Second):
assert.Fail(t, "context should be done")
}
assert.Equal(t, 1, *infoCount)
}
func TestWithTimeout(t *testing.T) {
parentCtx, infoCount := infoCounterContext(t)
ctx, cancel := WithTimeout(parentCtx, 10*time.Millisecond)
defer cancel()
assert.Equal(t, 0, *infoCount)
select {
case <-ctx.Done():
ctx.Logger().Info("yay")
case <-time.After(1 * time.Second):
assert.Fail(t, "context should be done")
}
assert.Equal(t, 1, *infoCount)
ctx, cancel = WithTimeout(parentCtx, 1*time.Second)
defer cancel()
select {
case <-ctx.Done():
assert.Fail(t, "context should not be done")
case <-time.After(10 * time.Millisecond):
ctx.Logger().Info("yay")
}
assert.Equal(t, 2, *infoCount)
}
func TestWithLogger(t *testing.T) {
var infoCount int
logger := testLogger(t, func(e zapcore.Entry) {
if e.Level == zap.InfoLevel {
infoCount++
}
})
ctx := WithLogger(context.Background(), logger)
assert.Equal(t, logger, ctx.Logger())
assert.Equal(t, 0, infoCount)
ctx.Logger().Info("yay")
assert.Equal(t, 1, infoCount)
}
func TestAsContext(t *testing.T) {
var gotValue any
normalFuncThatTakesContext := func(ctx context.Context) {
if logCtx, ok := ctx.(Context); ok {
logCtx.Logger().Info("yay")
}
gotValue = ctx.Value("key")
}
parentCtx, infoCount := infoCounterContext(t)
ctx := WithValue(parentCtx, "key", "value")
assert.Equal(t, 0, *infoCount)
normalFuncThatTakesContext(ctx)
assert.Equal(t, 1, *infoCount)
assert.Equal(t, "value", gotValue)
}
func TestWithValues(t *testing.T) {
var buffer bytes.Buffer
logger, sync := log.New("test",
log.WithConsoleSink(&buffer),
)
defer func(prevLogger logr.Logger) {
defaultLogger = prevLogger
}(defaultLogger)
SetDefaultLogger(logger)
{
ctx1 := Background()
ctx1.Logger().Info("only a", "a", 0)
ctx2 := WithValue(ctx1, "b", 1)
ctx2.Logger().Info("only b")
assert.Equal(t, 1, ctx2.Value("b"))
ctx3 := WithLogger(ctx2, ctx2.Logger().WithValues("c", 2, "d", 3))
ctx3.Logger().Info("bcd")
ctx2.Logger().Info("only b again")
type customKey string
ctx4 := WithValue(Background(), customKey("foo"), "bar")
// foo:bar shouldn't be added to the logger because the key isn't a string
ctx4.Logger().Info("foo")
ctx5 := WithValues(ctx2, "e", 4, "f", 5, 6, "six")
ctx5.Logger().Info("bef")
assert.Equal(t, "six", ctx5.Value(6))
ctx6 := WithValues(ctx2, "what does this do?")
ctx6.Logger().Info("silently fail I suppose")
}
assert.Nil(t, sync())
logs := strings.Split(strings.TrimSpace(buffer.String()), "\n")
assert.Equal(t, 7, len(logs))
assert.Contains(t, logs[0], `{"a": 0}`)
assert.Contains(t, logs[1], `{"b": 1}`)
assert.Contains(t, logs[2], `{"b": 1, "c": 2, "d": 3}`)
assert.Contains(t, logs[3], `{"b": 1}`)
assert.NotContains(t, logs[4], `{"foo": "bar"}`)
assert.Contains(t, logs[5], `{"b": 1, "e": 4, "f": 5}`)
assert.Contains(t, logs[6], `silently fail`)
assert.NotContains(t, logs[6], `what does this do?`)
}
func TestDefaultLogger(t *testing.T) {
var panicked bool
defer func() {
if r := recover(); r != nil {
panicked = true
}
assert.False(t, panicked)
}()
ctx := Background()
ctx.Logger().Info("this shouldn't panic")
}
func TestRace(t *testing.T) {
ctx, cancel := WithCancel(Background())
go cancel()
go func() { _ = ctx.Err() }()
cancel()
_ = ctx.Err()
}
func TestCause(t *testing.T) {
ctx, cancel := WithCancelCause(Background())
err := fmt.Errorf("oh no")
cancel(err)
assert.Equal(t, err, Cause(ctx))
}
// TestBuriedLogger tests when a logging context is wrapped by a non-logging
// implementation that we can still regain the original logger.
func TestBuriedLogger(t *testing.T) {
var buffer bytes.Buffer
logger, sync := log.New("test",
log.WithConsoleSink(&buffer),
)
defer func(prevLogger logr.Logger) {
defaultLogger = prevLogger
}(defaultLogger)
SetDefaultLogger(logger)
// Create a context with a key/value log entry.
ctx := WithValue(Background(), "log", "entry")
// Convert it to a stdlib context.
stdCtx := context.WithValue(ctx, "std", "entry") //nolint:staticcheck
// Try to get the logger back again.
ctx = AddLogger(stdCtx)
ctx.Logger().Info("test")
assert.Nil(t, sync())
logs := strings.Split(strings.TrimSpace(buffer.String()), "\n")
// Logger has the key/value log entry.
assert.Equal(t, 1, len(logs))
assert.Contains(t, logs[0], `{"log": "entry"}`)
// Grab bag still has both values.
assert.Equal(t, "entry", ctx.Value("log"))
assert.Equal(t, "entry", ctx.Value("std"))
}
================================================
FILE: pkg/custom_detectors/CUSTOM_DETECTORS.md
================================================
# TruffleHog Custom Detector Setup Guide
This guide will walk you through setting up a custom detector in TruffleHog to identify specific patterns unique to your project. For users of Trufflehog Enterprise, this guide applies to that environment as well.
## Steps to Set Up a Custom Detector
1. **Create a Configuration File**:
- TruffleHog uses a configuration file, typically named `config.yaml`, to manage custom detector configuration.
- If this file doesn't exist, create it in your system.
2. **Define the Custom Detector**:
- Open `config.yaml` with a text editor.
- Add a new detector under the `detectors` section.
Here's a template for a custom detector:
```yaml
# config.yaml
detectors:
- name: HogTokenDetector
keywords:
- hog
regex:
token: '[^A-Za-z0-9+\/]{0,1}([A-Za-z0-9+\/]{40})[^A-Za-z0-9+\/]{0,1}'
verify:
- endpoint: http://localhost:8000/
# 'unsafe' must be set to true if the endpoint uses HTTP
unsafe: true
headers:
- "Authorization: super secret authorization header"
```
**Explanation**:
- **`name`**: A unique identifier for your custom detector.
- **`keywords`**: An array of strings that, when found, trigger the regex search. If multiple keywords are specified, the presence of any one of them will initiate the regex search.
- **`regex`**: Defines the patterns to identify potential secrets. You can specify one or more named regular expressions. For a detection to be successful, each named regex must find a match. Capture groups `()` within these regular expressions are used to extract specific portions of the matched text, enabling the detector to process and report on particular segments of the identified patterns.
- **`verify`**: An optional section to validate detected secrets. If you want to verify or unverify detected secrets, this section needs to be configured. If not configured, all detected secrets will be marked as unverified. Read [verification server examples](#verification-server-examples)
**Other allowed parameters:**
- **`primary_regex_name`**: This parameter allows you designate the primary regex pattern when multiple regex patterns are defined in the regex section. If a match is found, the match for the designated primary regex will be used to determine the line number. The value must be one of the names specified in the regex section. If not provided, the first regex name in sorted order will be used as the primary regex by default.
- **`exclude_regexes_capture`**: This parameter allows you to define regex patterns to exclude specific parts of a detected secret. If a match is found within the detected secret, the portion matching this regex is excluded from the result.
- **`exclude_regexes_match`**: This parameter enables you to define regex patterns to exclude entire matches from being reported as secrets. This applies to the entire matched string, not just the token.
- **`entropy`**: This parameter is used to assess the randomness of detected strings. High entropy often indicates that a string is a potential secret, such as an API key or password, due to its complexity and unpredictability. It helps in filtering false-positives. While an entropy threshold of `3` can be a starting point, it's essential to adjust this value based on your project's specific requirements and the nature of the data you have.
- **`exclude_words`**: This parameter allows you to specify a list of words that, if present in a detected string, will cause TruffleHog to ignore that string. This is a substring match and does not enforce word boundaries. It applies only to the token.
- **`validations`**: This parameter lets you define extra validation rules for each regex specified in the regex option. These rules address limitations of Go's RE2 engine, such as the lack of lookahead support, and are applied after a regex match to help reduce false positives.
Available validation options:
- **`contains_digit`**: Ensures the match contains at least one numeric digit (0-9). Useful for API keys or tokens that must include numbers.
- **`contains_lowercase`**: Ensures the match contains at least one lowercase letter (a-z). Common requirement for passwords and mixed-case tokens.
- **`contains_uppercase`**: Ensures the match contains at least one uppercase letter (A-Z). Helps validate tokens that follow mixed-case conventions.
- **`contains_special_char`**: Ensures the match contains at least one special character from the set `!@#$%^&*()_+-=[]{}|;:,.<>?`. Useful for complex passwords or encoded tokens.
[Here](/examples/generic_with_filters.yml) is an example of a custom detector using these parameters.
3. **Run TruffleHog with the Custom Detector**:
- Execute TruffleHog, specifying your configuration file:
```bash
trufflehog filesystem --config=/config.yaml
```
- Replace `` with the path to the directory or file you want to scan, and `` with the path to your `config.yaml`.
- TruffleHog will scan the specified file or folder using the custom detector you've defined.
4. **Example**:
Let's use the template config provided above to search a file.
Assume you have a file `/tmp/data.txt` with the following content:
```text
// this is a custom example
this file has some random text and maybe a secret
hog token: pOIAj9x47WT5qElx5JrI3e7O714HgaAIz2ck9sVn
// end of file
```
In this file, the keyword `hog` exists, which will trigger the regex search. The string `pOIAj9x47WT5qElx5JrI3e7O714HgaAIz2ck9sVn` matches the regex pattern, so it should be detected.
Run the following command:
```bash
trufflehog filesystem /tmp --config=config.yaml
```
The output should be similar to:
```
🐷🔑🐷 TruffleHog. Unearth your secrets. 🐷🔑🐷
Found verified result 🐷🔑
Detector Type: CustomRegex
Decoder Type: PLAIN
Raw result: pOIAj9x47WT5qElx5JrI3e7O714HgaAIz2ck9sVn
File: /tmp/data.txt
Line: 3
```
The `Raw result` contains the matched string. `File` is the file name where secret was detected and `Line` is the exact line in the file where that was found.
## Verification Server Examples
Unless you run a verification server, secrets found by the custom regex detector will be unverified. Here is an example Python and Go implementation of a verification server for the above config.yaml file.
### Python:
```python
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
AUTH_HEADER = 'super secret authorization header'
class Verifier(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(405)
self.end_headers()
def do_POST(self):
try:
if self.headers['Authorization'] != AUTH_HEADER:
self.send_response(401)
self.end_headers()
return
length = int(self.headers['Content-Length'])
request = json.loads(self.rfile.read(length))
self.log_message("%s", request)
if not validateTokens(request['HogTokenDetector']['token']):
self.send_response(200)
self.end_headers()
else:
self.send_response(403)
self.end_headers()
except Exception:
self.send_response(400)
self.end_headers()
def validateTokens(token):
return False # Implement actual validation logic
with HTTPServer(('', 8000), Verifier) as server:
try:
server.serve_forever()
except KeyboardInterrupt:
pass
```
### Go
```go
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
const authHeader = "super secret authorization header"
type HogTokenDetector struct {
Token string `json:"token"`
}
type RequestBody struct {
HogTokenDetector HogTokenDetector `json:"HogTokenDetector"`
}
func validateTokens(token string) bool {
return false // Implement actual validation logic
}
func verifierHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
if r.Header.Get("Authorization") != authHeader {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
defer r.Body.Close()
var requestBody RequestBody
if err := json.Unmarshal(body, &requestBody); err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
log.Printf("Received Request: %+v", requestBody)
if validateTokens(requestBody.HogTokenDetector.Token) {
http.Error(w, "Forbidden", http.StatusForbidden)
} else {
w.WriteHeader(http.StatusOK)
}
}
func main() {
http.HandleFunc("/", verifierHandler)
serverAddr := ":8000"
fmt.Printf("Starting server on %s...\n", serverAddr)
if err := http.ListenAndServe(serverAddr, nil); err != nil {
log.Fatalf("Server failed: %s", err)
}
}
```
================================================
FILE: pkg/custom_detectors/custom_detectors.go
================================================
package custom_detectors
import (
"bytes"
"context"
"encoding/json"
"io"
"maps"
"net/http"
"regexp"
"slices"
"strings"
"golang.org/x/sync/errgroup"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/custom_detectorspb"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
// The maximum number of matches from one chunk. This const is used when
// permutating each regex match to protect the scanner from doing too much work
// for poorly defined regexps.
const maxTotalMatches = 100
// CustomRegexWebhook is a CustomRegex with webhook validation that is
// guaranteed to be valid (assuming the data is not changed after
// initialization).
type CustomRegexWebhook struct {
*custom_detectorspb.CustomRegex
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*CustomRegexWebhook)(nil)
var _ detectors.CustomFalsePositiveChecker = (*CustomRegexWebhook)(nil)
var _ detectors.MaxSecretSizeProvider = (*CustomRegexWebhook)(nil)
// NewWebhookCustomRegex initializes and validates a CustomRegexWebhook. An
// unexported type is intentionally returned here to ensure the values have
// been validated.
func NewWebhookCustomRegex(pb *custom_detectorspb.CustomRegex) (*CustomRegexWebhook, error) {
// TODO: Return all validation errors.
if err := ValidateKeywords(pb.Keywords); err != nil {
return nil, err
}
if err := ValidateRegex(pb.Regex); err != nil {
return nil, err
}
if err := ValidateRegexSlice(pb.ExcludeRegexesCapture); err != nil {
return nil, err
}
if err := ValidateRegexSlice(pb.ExcludeRegexesMatch); err != nil {
return nil, err
}
if err := ValidatePrimaryRegexName(pb.PrimaryRegexName, pb.Regex); err != nil {
return nil, err
}
for _, verify := range pb.Verify {
if err := ValidateVerifyEndpoint(verify.Endpoint, verify.Unsafe); err != nil {
return nil, err
}
if err := ValidateVerifyHeaders(verify.Headers); err != nil {
return nil, err
}
}
// Ensure primary regex name is set.
ensurePrimaryRegexNameSet(pb)
// TODO: Copy only necessary data out of pb.
return &CustomRegexWebhook{pb}, nil
}
var httpClient = common.SaneHttpClient()
func (c *CustomRegexWebhook) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
dataStr := string(data)
regexMatches := make(map[string][][]string, len(c.GetRegex()))
// Compile exclude regexes targeting the capture group
excludeRegexesCapture := make([]*regexp.Regexp, 0, len(c.GetExcludeRegexesCapture()))
for _, exclude := range c.GetExcludeRegexesCapture() {
regex, err := regexp.Compile(exclude)
if err != nil {
// This will only happen if the regex is invalid.
return nil, err
}
excludeRegexesCapture = append(excludeRegexesCapture, regex)
}
// Compile exclude regexes targeting the entire match
excludeRegexes := make([]*regexp.Regexp, 0, len(c.GetExcludeRegexesMatch()))
for _, exclude := range c.GetExcludeRegexesMatch() {
regex, err := regexp.Compile(exclude)
if err != nil {
// This will only happen if the regex is invalid.
return nil, err
}
excludeRegexes = append(excludeRegexes, regex)
}
// Find all submatches for each regex.
for name, regex := range c.GetRegex() {
regex, err := regexp.Compile(regex)
if err != nil {
// This will only happen if the regex is invalid.
return nil, err
}
regexMatches[name] = regex.FindAllStringSubmatch(dataStr, -1)
}
// Permutate each individual match.
// {
// "foo": [["match1"]]
// "bar": [["match2"], ["match3"]]
// }
// becomes
// [
// {"foo": ["match1"], "bar": ["match2"]},
// {"foo": ["match1"], "bar": ["match3"]},
// ]
matches := permutateMatches(regexMatches)
g := new(errgroup.Group)
// Create result object and test for verification.
resultsCh := make(chan detectors.Result, maxTotalMatches)
MatchLoop:
for _, match := range matches {
for key, values := range match {
// attempt to use capture group
secret := values[0]
if len(values) > 1 {
secret = values[1]
}
// check entropy
entropy := c.GetEntropy()
if entropy > 0.0 && detectors.StringShannonEntropy(secret) < float64(entropy) {
continue MatchLoop
}
// check for exclude words
for _, excludeWord := range c.GetExcludeWords() {
if strings.Contains(strings.ToLower(secret), excludeWord) {
continue MatchLoop
}
}
// exclude checks
for _, excludeMatch := range excludeRegexes {
if excludeMatch.MatchString(values[0]) {
continue MatchLoop
}
}
// exclude secret (capture group), or if no capture group is set,
// check against entire match.
for _, excludeSecret := range excludeRegexesCapture {
if excludeSecret.MatchString(secret) {
continue MatchLoop
}
}
if validations := c.GetValidations(); validations != nil {
validationRules := []struct {
enabled bool
validator func(string) bool
}{
{validations[key].GetContainsDigit(), ContainsDigit},
{validations[key].GetContainsLowercase(), ContainsLowercase},
{validations[key].GetContainsUppercase(), ContainsUppercase},
{validations[key].GetContainsSpecialChar(), ContainsSpecialChar},
}
for _, rule := range validationRules {
if rule.enabled && !rule.validator(secret) {
// skip this match if a validation rule is enabled but missing from the secret
continue MatchLoop
}
}
}
}
g.Go(func() error {
return c.createResults(ctx, match, verify, resultsCh)
})
}
// Ignore any errors and collect as many of the results as we can.
_ = g.Wait()
close(resultsCh)
for result := range resultsCh {
if result.ExtraData != nil {
result.ExtraData["name"] = c.GetName()
}
results = append(results, result)
}
return results, nil
}
func (c *CustomRegexWebhook) IsFalsePositive(_ detectors.Result) (bool, string) {
return false, ""
}
// custom max size for custom detector
func (c *CustomRegexWebhook) MaxSecretSize() int64 {
return 1000
}
func (c *CustomRegexWebhook) createResults(ctx context.Context, match map[string][]string, verify bool, results chan<- detectors.Result) error {
if common.IsDone(ctx) {
// TODO: Log we're possibly leaving out results.
return ctx.Err()
}
result := detectors.Result{
DetectorType: detectorspb.DetectorType_CustomRegex,
DetectorName: c.GetName(),
ExtraData: map[string]string{},
}
var raw string
for _, key := range slices.Sorted(maps.Keys(match)) {
values := match[key]
// values[0] contains the entire regex match.
secret := values[0]
fullMatch := values[0]
if len(values) > 1 {
secret = values[1]
}
raw += secret
// We set the full regex match as the primary secret value.
// Reasoning:
// The engine calculates the line number using the match. When a primary secret is set, it uses that value instead of the raw secret.
// While the secret match itself is sufficient to calculate the line number, the same group match could appear elsewhere in the data.
// To avoid ambiguity, we store the full regex match as the primary secret value.
// This primary secret value is used only for identifying the exact line number and is not used anywhere else.
// Example:
// Full regex match: secret = ABC123
// Secret (raw): ABC123
// In this case, the primary secret value stores the full string `secret = ABC123`,
// allowing the engine to pinpoint the exact location and avoid matching redundant occurrences of `ABC123` in the data.
if c.PrimaryRegexName == key {
result.SetPrimarySecretValue(fullMatch)
}
}
result.Raw = []byte(raw)
if !verify {
select {
case <-ctx.Done():
return ctx.Err()
case results <- result:
return nil
}
}
// Verify via webhook.
jsonBody, err := json.Marshal(map[string]map[string][]string{
c.GetName(): match,
})
if err != nil {
// This should never happen, but if it does, return nil to not
// disrupt other verification.
return nil
}
// Try each config until we successfully verify.
for _, verifyConfig := range c.GetVerify() {
if common.IsDone(ctx) {
// TODO: Log we're possibly leaving out results.
return ctx.Err()
}
req, err := http.NewRequestWithContext(ctx, "POST", verifyConfig.GetEndpoint(), bytes.NewReader(jsonBody))
if err != nil {
continue
}
for _, header := range verifyConfig.GetHeaders() {
key, value, found := strings.Cut(header, ":")
if !found {
// Should be unreachable due to validation.
continue
}
req.Header.Add(key, strings.TrimLeft(value, "\t\n\v\f\r "))
}
resp, err := httpClient.Do(req)
if err != nil {
continue
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
if resp.StatusCode == http.StatusOK {
// mark the result as verified
result.Verified = true
body, err := io.ReadAll(resp.Body)
if err != nil {
continue
}
// TODO: handle different content-type responses seperatly when implement custom detector configurations
responseStr := string(body)
// truncate to 200 characters if response length exceeds 200
if len(responseStr) > 200 {
responseStr = responseStr[:200]
}
// store the processed response in ExtraData
result.ExtraData["response"] = responseStr
break
}
}
select {
case <-ctx.Done():
return ctx.Err()
case results <- result:
return nil
}
}
func (c *CustomRegexWebhook) Keywords() []string {
return c.GetKeywords()
}
// productIndices produces a permutation of indices for each length. Example:
// productIndices(3, 2) -> [[0 0] [1 0] [2 0] [0 1] [1 1] [2 1]]. It returns
// a slice of length no larger than maxTotalMatches.
func productIndices(lengths ...int) [][]int {
count := 1
for _, l := range lengths {
count *= l
}
if count == 0 {
return nil
}
if count > maxTotalMatches {
count = maxTotalMatches
}
results := make([][]int, count)
for i := 0; i < count; i++ {
j := 1
result := make([]int, 0, len(lengths))
for _, l := range lengths {
result = append(result, (i/j)%l)
j *= l
}
results[i] = result
}
return results
}
// permutateMatches converts the list of all regex matches into all possible
// permutations selecting one from each named entry in the map. For example:
// {"foo": [matchA, matchB], "bar": [matchC]} becomes
//
// [{"foo": matchA, "bar": matchC}, {"foo": matchB, "bar": matchC}]
func permutateMatches(regexMatches map[string][][]string) []map[string][]string {
// Get a consistent order for names and their matching lengths.
// The lengths are used in calculating the permutation so order matters.
names := make([]string, 0, len(regexMatches))
lengths := make([]int, 0, len(regexMatches))
for key, value := range regexMatches {
names = append(names, key)
lengths = append(lengths, len(value))
}
// Permutate all the indices for each match. For example, if "foo" has
// [matchA, matchB] and "bar" has [matchC], we will get indices [0 0] [1 0].
permutationIndices := productIndices(lengths...)
// Build {"foo": matchA, "bar": matchC} and {"foo": matchB, "bar": matchC}
// from the indices.
var matches []map[string][]string
for _, permutation := range permutationIndices {
candidate := make(map[string][]string, len(permutationIndices))
for i, name := range names {
candidate[name] = regexMatches[name][permutation[i]]
}
matches = append(matches, candidate)
}
return matches
}
func (c *CustomRegexWebhook) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CustomRegex
}
const defaultDescription = "This is a user-defined detector with no description provided."
func (c *CustomRegexWebhook) Description() string {
if c.GetDescription() == "" {
return defaultDescription
}
return c.GetDescription()
}
// ensurePrimaryRegexNameSet sets the PrimaryRegexName field to the
// first regex name in sorted order if it is not already set.
// We're sorting to ensure deterministic behavior.
func ensurePrimaryRegexNameSet(pb *custom_detectorspb.CustomRegex) {
if pb.PrimaryRegexName == "" {
for _, name := range slices.Sorted(maps.Keys(pb.Regex)) {
pb.PrimaryRegexName = name
return
}
}
}
================================================
FILE: pkg/custom_detectors/custom_detectors_test.go
================================================
package custom_detectors
import (
"context"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/custom_detectorspb"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
"github.com/trufflesecurity/trufflehog/v3/pkg/protoyaml"
)
func TestCustomRegexTemplateParsing(t *testing.T) {
testCustomRegexTemplateYaml := `name: Internal bi tool
keywords:
- secret_v1_
- pat_v2_
regex:
id_pat_example: ([a-zA-Z0-9]{32})
secret_pat_example: ([a-zA-Z0-9]{32})
verify:
- endpoint: http://localhost:8000/{id_pat_example}
unsafe: true
headers:
- 'Authorization: Bearer {secret_pat_example.0}'
successRanges:
- 200-250
- '288'`
var got custom_detectorspb.CustomRegex
assert.NoError(t, protoyaml.UnmarshalStrict([]byte(testCustomRegexTemplateYaml), &got))
assert.Equal(t, "Internal bi tool", got.Name)
assert.Equal(t, []string{"secret_v1_", "pat_v2_"}, got.Keywords)
assert.Equal(t, map[string]string{
"id_pat_example": "([a-zA-Z0-9]{32})",
"secret_pat_example": "([a-zA-Z0-9]{32})",
}, got.Regex)
assert.Equal(t, 1, len(got.Verify))
assert.Equal(t, "http://localhost:8000/{id_pat_example}", got.Verify[0].Endpoint)
assert.Equal(t, true, got.Verify[0].Unsafe)
assert.Equal(t, []string{"Authorization: Bearer {secret_pat_example.0}"}, got.Verify[0].Headers)
assert.Equal(t, []string{"200-250", "288"}, got.Verify[0].SuccessRanges)
}
func TestCustomRegexWebhookParsing(t *testing.T) {
testCustomRegexWebhookYaml := `name: Internal bi tool
keywords:
- secret_v1_
- pat_v2_
regex:
id_pat_example: ([a-zA-Z0-9]{32})
secret_pat_example: ([a-zA-Z0-9]{32})
verify:
- endpoint: http://localhost:8000/
unsafe: true
headers:
- 'Authorization: Bearer token'`
var got custom_detectorspb.CustomRegex
assert.NoError(t, protoyaml.UnmarshalStrict([]byte(testCustomRegexWebhookYaml), &got))
assert.Equal(t, "Internal bi tool", got.Name)
assert.Equal(t, []string{"secret_v1_", "pat_v2_"}, got.Keywords)
assert.Equal(t, map[string]string{
"id_pat_example": "([a-zA-Z0-9]{32})",
"secret_pat_example": "([a-zA-Z0-9]{32})",
}, got.Regex)
assert.Equal(t, 1, len(got.Verify))
assert.Equal(t, "http://localhost:8000/", got.Verify[0].Endpoint)
assert.Equal(t, true, got.Verify[0].Unsafe)
assert.Equal(t, []string{"Authorization: Bearer token"}, got.Verify[0].Headers)
}
// TestCustomDetectorsParsing tests the full `detectors` configuration.
func TestCustomDetectorsParsing(t *testing.T) {
// TODO: Support both template and webhook.
testYamlConfig := `detectors:
- name: Internal bi tool
keywords:
- secret_v1_
- pat_v2_
regex:
id_pat_example: ([a-zA-Z0-9]{32})
secret_pat_example: ([a-zA-Z0-9]{32})
verify:
- endpoint: http://localhost:8000/
unsafe: true
headers:
- 'Authorization: Bearer token'`
var messages custom_detectorspb.CustomDetectors
assert.NoError(t, protoyaml.UnmarshalStrict([]byte(testYamlConfig), &messages))
assert.Equal(t, 1, len(messages.Detectors))
got := messages.Detectors[0]
assert.Equal(t, "Internal bi tool", got.Name)
assert.Equal(t, []string{"secret_v1_", "pat_v2_"}, got.Keywords)
assert.Equal(t, map[string]string{
"id_pat_example": "([a-zA-Z0-9]{32})",
"secret_pat_example": "([a-zA-Z0-9]{32})",
}, got.Regex)
assert.Equal(t, 1, len(got.Verify))
assert.Equal(t, "http://localhost:8000/", got.Verify[0].Endpoint)
assert.Equal(t, true, got.Verify[0].Unsafe)
assert.Equal(t, []string{"Authorization: Bearer token"}, got.Verify[0].Headers)
}
func TestFromData_InvalidRegEx(t *testing.T) {
c := &CustomRegexWebhook{
&custom_detectorspb.CustomRegex{
Name: "Internal bi tool",
Keywords: []string{"secret_v1_", "pat_v2_"},
Regex: map[string]string{
"test": "!!?(?:?)[a-zA-Z0-9]{32}", // invalid regex
},
},
}
_, err := c.FromData(context.Background(), false, []byte("test"))
assert.Error(t, err)
}
func TestProductIndices(t *testing.T) {
tests := []struct {
name string
input []int
want [][]int
}{
{
name: "zero",
input: []int{3, 0},
want: nil,
},
{
name: "one input",
input: []int{3},
want: [][]int{{0}, {1}, {2}},
},
{
name: "two inputs",
input: []int{3, 2},
want: [][]int{
{0, 0}, {1, 0}, {2, 0},
{0, 1}, {1, 1}, {2, 1},
},
},
{
name: "three inputs",
input: []int{3, 2, 3},
want: [][]int{
{0, 0, 0}, {1, 0, 0}, {2, 0, 0},
{0, 1, 0}, {1, 1, 0}, {2, 1, 0},
{0, 0, 1}, {1, 0, 1}, {2, 0, 1},
{0, 1, 1}, {1, 1, 1}, {2, 1, 1},
{0, 0, 2}, {1, 0, 2}, {2, 0, 2},
{0, 1, 2}, {1, 1, 2}, {2, 1, 2},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := productIndices(tt.input...)
assert.Equal(t, tt.want, got)
})
}
}
func TestProductIndicesMax(t *testing.T) {
got := productIndices(2, 3, 4, 5, 6)
assert.GreaterOrEqual(t, 2*3*4*5*6, maxTotalMatches)
assert.Equal(t, maxTotalMatches, len(got))
}
func TestPermutateMatches(t *testing.T) {
tests := []struct {
name string
input map[string][][]string
want []map[string][]string
}{
{
name: "two matches",
input: map[string][][]string{"foo": {{"matchA"}, {"matchB"}}, "bar": {{"matchC"}}},
want: []map[string][]string{
{"foo": {"matchA"}, "bar": {"matchC"}},
{"foo": {"matchB"}, "bar": {"matchC"}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := permutateMatches(tt.input)
assert.Equal(t, tt.want, got)
})
}
}
func TestDetector(t *testing.T) {
detector, err := NewWebhookCustomRegex(&custom_detectorspb.CustomRegex{
Name: "test",
// "password" is normally flagged as a false positive, but CustomRegex
// should allow the user to decide and report it as a result.
Keywords: []string{"password"},
Regex: map[string]string{"regex": "password=\"(.*)\""},
})
assert.NoError(t, err)
results, err := detector.FromData(context.Background(), false, []byte(`password="123456"`))
assert.NoError(t, err)
assert.Equal(t, 1, len(results))
assert.Equal(t, results[0].Raw, []byte(`123456`))
}
func TestDetectorPrimarySecret(t *testing.T) {
detector, err := NewWebhookCustomRegex(&custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"secret"},
Regex: map[string]string{"id": "id_[A-Z0-9]{10}_yy", "secret": "secret_[A-Z0-9]{10}_yy"},
PrimaryRegexName: "secret",
})
assert.NoError(t, err)
results, err := detector.FromData(context.Background(), false, []byte(`
// getData returns id and secret
func getData()(string, string){
return "id_ALPHA10100_yy", "secret_YI7C90ACY1_yy"
}
`))
assert.NoError(t, err)
assert.Equal(t, 1, len(results))
assert.Equal(t, "secret_YI7C90ACY1_yy", results[0].GetPrimarySecretValue())
}
func TestDetectorPrimarySecretFullMatch(t *testing.T) {
tests := []struct {
name string
input *custom_detectorspb.CustomRegex
chunk []byte
want string
}{
{
name: "primary regex full match",
input: &custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"secret"},
Regex: map[string]string{"secret": `secret *= *"([^"\r\n]+)"`},
PrimaryRegexName: "secret",
},
chunk: []byte(`
// some code
secret="mysecret"
// some code
`),
want: `secret="mysecret"`,
},
{
name: "primary regex full match multiline",
input: &custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"secret"},
Regex: map[string]string{"secret": `secret *= *"([^"]+)"`},
PrimaryRegexName: "secret",
},
chunk: []byte(`
// some code
secret="mysecret
thatspansmultiplelines"
// some code
`),
want: `secret="mysecret
thatspansmultiplelines"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
detector, err := NewWebhookCustomRegex(tt.input)
assert.NoError(t, err)
results, err := detector.FromData(context.Background(), false, tt.chunk)
assert.NoError(t, err)
assert.Equal(t, 1, len(results))
assert.Equal(t, tt.want, results[0].GetPrimarySecretValue())
})
}
}
func TestDetectorValidations(t *testing.T) {
type args struct {
CustomRegex *custom_detectorspb.CustomRegex
Data string
}
tests := []struct {
name string
input args
want []detectors.Result
}{
{
name: "custom validation - contains digit",
input: args{
CustomRegex: &custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"password"},
Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`},
Validations: map[string]*custom_detectorspb.ValidationConfig{
"password": {
ContainsDigit: true,
},
},
},
Data: `This is custom example
This file has a random text and maybe a secret
Password: MyStr0ngP@ssword!
End of file`,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CustomRegex,
DetectorName: "test",
Verified: false,
Raw: []byte("MyStr0ngP@ssword!"),
},
},
},
{
name: "custom validation - does not contains digit",
input: args{
CustomRegex: &custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"password"},
Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`},
Validations: map[string]*custom_detectorspb.ValidationConfig{
"password": {
ContainsDigit: true,
},
},
},
Data: `This is custom example
This file has a random text and maybe a secret
Password: MyStrongPassword!
End of file`,
},
want: nil,
},
{
name: "custom validation - contains lowercase",
input: args{
CustomRegex: &custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"password"},
Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`},
Validations: map[string]*custom_detectorspb.ValidationConfig{
"password": {
ContainsLowercase: true,
},
},
},
Data: `This is custom example
This file has a random text and maybe a secret
Password: MyStrongPassword!
End of file`,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CustomRegex,
DetectorName: "test",
Verified: false,
Raw: []byte("MyStrongPassword!"),
},
},
},
{
name: "custom validation - does not contains lowercase",
input: args{
CustomRegex: &custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"password"},
Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`},
Validations: map[string]*custom_detectorspb.ValidationConfig{
"password": {
ContainsLowercase: true,
},
},
},
Data: `This is custom example
This file has a random text and maybe a secret
Password: MYSTRONGPASSWORD!
End of file`,
},
want: nil,
},
{
name: "custom validation - contains uppercase",
input: args{
CustomRegex: &custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"password"},
Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`},
Validations: map[string]*custom_detectorspb.ValidationConfig{
"password": {
ContainsUppercase: true,
},
},
},
Data: `This is custom example
This file has a random text and maybe a secret
Password: MyStrongPassword!
End of file`,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CustomRegex,
DetectorName: "test",
Verified: false,
Raw: []byte("MyStrongPassword!"),
},
},
},
{
name: "custom validation - does not contains uppercase",
input: args{
CustomRegex: &custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"password"},
Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`},
Validations: map[string]*custom_detectorspb.ValidationConfig{
"password": {
ContainsUppercase: true,
},
},
},
Data: `This is custom example
This file has a random text and maybe a secret
Password: mystrongpassword!
End of file`,
},
want: nil,
},
{
name: "custom validation - contains special character",
input: args{
CustomRegex: &custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"password"},
Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`},
Validations: map[string]*custom_detectorspb.ValidationConfig{
"password": {
ContainsSpecialChar: true,
},
},
},
Data: `This is custom example
This file has a random text and maybe a secret
Password: MyStr@ngP@ssword!
End of file`,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CustomRegex,
DetectorName: "test",
Verified: false,
Raw: []byte("MyStr@ngP@ssword!"),
},
},
},
{
name: "custom validation - does not contains special character",
input: args{
CustomRegex: &custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"password"},
Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`},
Validations: map[string]*custom_detectorspb.ValidationConfig{
"password": {
ContainsSpecialChar: true,
},
},
},
Data: `This is custom example
This file has a random text and maybe a secret
Password: MyStrongPassword
End of file`,
},
want: nil,
},
{
name: "custom validation - contains uppercase and special characters",
input: args{
CustomRegex: &custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"password"},
Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`},
Validations: map[string]*custom_detectorspb.ValidationConfig{
"password": {
ContainsUppercase: true,
ContainsSpecialChar: true,
},
},
},
Data: `This is custom example
This file has a random text and maybe a secret
Password: MyStrongP@ssword
End of file`,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CustomRegex,
DetectorName: "test",
Verified: false,
Raw: []byte("MyStrongP@ssword"),
},
},
},
{
name: "custom validation - contains uppercase but does not contain special characters",
input: args{
CustomRegex: &custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"password"},
Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`},
Validations: map[string]*custom_detectorspb.ValidationConfig{
"password": {
ContainsUppercase: true,
ContainsSpecialChar: true,
},
},
},
Data: `This is custom example
This file has a random text and maybe a secret
Password: MyStrongPassword
End of file`,
},
want: nil,
},
{
name: "custom validation - wrong regex name in validations",
input: args{
CustomRegex: &custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"password"},
Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`},
Validations: map[string]*custom_detectorspb.ValidationConfig{
"wrong": {
ContainsUppercase: true,
},
},
},
Data: `This is custom example
This file has a random text and maybe a secret
Password: mystrongp@ssword
End of file`,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CustomRegex,
DetectorName: "test",
Verified: false,
Raw: []byte("mystrongp@ssword"),
},
},
},
{
name: "custom validation - multiple regex validations",
input: args{
CustomRegex: &custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"password", "api_key"},
Regex: map[string]string{
"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`,
"api_key": `([a-f0-9_-]{32})`,
},
Validations: map[string]*custom_detectorspb.ValidationConfig{
"password": {
ContainsUppercase: true,
ContainsSpecialChar: true,
},
"api_key": {
ContainsSpecialChar: true,
},
},
},
Data: `This is custom example
This file has a random text and maybe a secret
Password: MyStrongP@ssword
API_Key: c392c9837d69b44c764cbf260b-e6184 // should be detected
API_Key: c392c9837d69b44c764cbf260be6184 // should be filtered by validation
End of file`,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CustomRegex,
DetectorName: "test",
Verified: false,
Raw: []byte("c392c9837d69b44c764cbf260b-e6184MyStrongP@ssword"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
detector, err := NewWebhookCustomRegex(tt.input.CustomRegex)
assert.NoError(t, err)
results, err := detector.FromData(context.Background(), false, []byte(tt.input.Data))
assert.NoError(t, err)
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "ExtraData", "verificationError", "primarySecret")
if diff := cmp.Diff(results, tt.want, ignoreOpts); diff != "" {
t.Errorf("CustomDetector.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
}
})
}
}
func TestNewWebhookCustomRegex_Validation(t *testing.T) {
t.Parallel()
// A known-good baseline; each test case mutates exactly one thing to trigger a specific validator.
base := func() *custom_detectorspb.CustomRegex {
return &custom_detectorspb.CustomRegex{
Name: "ok",
Keywords: []string{"kw"},
Regex: map[string]string{
"main": `\btoken_[a-z]+\b`,
},
PrimaryRegexName: "main",
ExcludeRegexesCapture: []string{
`^skip_.*$`,
},
ExcludeRegexesMatch: []string{
`^ignore_.*$`,
},
Verify: []*custom_detectorspb.VerifierConfig{
{
Endpoint: "https://example.com/verify",
Unsafe: false,
Headers: []string{"Authorization: Bearer x"},
},
},
}
}
tests := []struct {
name string
mutate func(*custom_detectorspb.CustomRegex)
wantErr bool
wantErrSubstr string // substring expected in error
}{
{
name: "Validate everything ok",
mutate: func(pb *custom_detectorspb.CustomRegex) {},
},
{
name: "ValidateKeywords: no keywords",
mutate: func(pb *custom_detectorspb.CustomRegex) {
pb.Keywords = nil
},
wantErr: true,
wantErrSubstr: "no keywords",
},
{
name: "ValidateKeywords: empty keyword",
mutate: func(pb *custom_detectorspb.CustomRegex) {
pb.Keywords = []string{""}
},
wantErr: true,
wantErrSubstr: "empty keyword",
},
{
name: "ValidateRegex: no regex",
mutate: func(pb *custom_detectorspb.CustomRegex) {
pb.Regex = nil
},
wantErr: true,
wantErrSubstr: "no regex",
},
{
name: "ValidateRegex: invalid regex in map",
mutate: func(pb *custom_detectorspb.CustomRegex) {
pb.Regex = map[string]string{"main": "("} // invalid
},
wantErr: true,
wantErrSubstr: "regex 'main':",
},
{
name: "ValidateRegexSlice: invalid exclude_regexes_capture",
mutate: func(pb *custom_detectorspb.CustomRegex) {
pb.ExcludeRegexesCapture = []string{"("} // invalid
},
wantErr: true,
wantErrSubstr: "regex '1':",
},
{
name: "ValidateRegexSlice: invalid exclude_regexes_match",
mutate: func(pb *custom_detectorspb.CustomRegex) {
pb.ExcludeRegexesMatch = []string{"("} // invalid
},
wantErr: true,
wantErrSubstr: "regex '1':",
},
{
name: "ValidatePrimaryRegexName: unknown primary regex name",
mutate: func(pb *custom_detectorspb.CustomRegex) {
pb.PrimaryRegexName = "does-not-exist"
},
wantErr: true,
wantErrSubstr: `unknown primary regex name: "does-not-exist"`,
},
{
name: "ValidateVerifyEndpoint: empty endpoint",
mutate: func(pb *custom_detectorspb.CustomRegex) {
pb.Verify = []*custom_detectorspb.VerifierConfig{
{Endpoint: "", Unsafe: false, Headers: []string{"A: b"}},
}
},
wantErr: true,
wantErrSubstr: "no endpoint",
},
{
name: "ValidateVerifyEndpoint: http endpoint without unsafe=true",
mutate: func(pb *custom_detectorspb.CustomRegex) {
pb.Verify = []*custom_detectorspb.VerifierConfig{
{Endpoint: "http://example.com/verify", Unsafe: false, Headers: []string{"A: b"}},
}
},
wantErr: true,
wantErrSubstr: "http endpoint must have unsafe=true",
},
{
name: "ValidateVerifyHeaders: header missing colon",
mutate: func(pb *custom_detectorspb.CustomRegex) {
pb.Verify = []*custom_detectorspb.VerifierConfig{
{Endpoint: "https://example.com/verify", Unsafe: false, Headers: []string{"Authorization Bearer x"}},
}
},
wantErr: true,
wantErrSubstr: `must contain a colon`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
pb := base()
tt.mutate(pb)
got, err := NewWebhookCustomRegex(pb)
if (err != nil) != tt.wantErr {
t.Fatalf("expected error=%v, got error=%v (result=%#v)", tt.wantErr, err != nil, got)
}
if tt.wantErr && got != nil {
t.Fatalf("expected nil result on error, got=%#v", got)
}
if tt.wantErr && !strings.Contains(err.Error(), tt.wantErrSubstr) {
t.Fatalf("error mismatch:\n got: %q\n want substring: %q", err.Error(), tt.wantErrSubstr)
}
})
}
}
func TestNewWebhookCustomRegex_EnsurePrimaryRegexNameSet(t *testing.T) {
t.Parallel()
pb := &custom_detectorspb.CustomRegex{
Name: "test",
Keywords: []string{"kw"},
Regex: map[string]string{
"regex_a": `regex_a`,
"regex_b": `regex_b`,
},
// PrimaryRegexName is not set.
}
detector, err := NewWebhookCustomRegex(pb)
assert.NoError(t, err)
assert.Equal(t, "regex_a", detector.GetPrimaryRegexName(), "expected PrimaryRegexName to be set to regex_a")
}
func BenchmarkProductIndices(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = productIndices(3, 2, 6)
}
}
================================================
FILE: pkg/custom_detectors/regex_varstring.go
================================================
package custom_detectors
import (
"regexp"
"strconv"
"strings"
)
// nameGroupRegex matches `{ name . group }` ignoring any whitespace.
var nameGroupRegex = regexp.MustCompile(`{\s*([a-zA-Z0-9-_]+)\s*(\.\s*[0-9]*)?\s*}`)
// RegexVarString is a string with embedded {name.group} variables. A name may
// only contain alphanumeric, hyphen, and underscore characters. Group is
// optional but if provided it must be a non-negative integer. If the group is
// omitted it defaults to 0.
type RegexVarString struct {
original string
// map from name to group
variables map[string]int
}
func NewRegexVarString(original string) RegexVarString {
variables := make(map[string]int)
matches := nameGroupRegex.FindAllStringSubmatch(original, -1)
for _, match := range matches {
name, group := match[1], 0
// The second match will start with a period followed by any number
// of whitespace.
if len(match[2]) > 1 {
g, err := strconv.Atoi(strings.TrimSpace(match[2][1:]))
if err != nil {
continue
}
group = g
}
variables[name] = group
}
return RegexVarString{
original: original,
variables: variables,
}
}
================================================
FILE: pkg/custom_detectors/regex_varstring_test.go
================================================
package custom_detectors
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestVarString(t *testing.T) {
tests := []struct {
name string
input string
wantVars map[string]int
}{
{
name: "empty",
input: "{}",
wantVars: map[string]int{},
},
{
name: "no subgroup",
input: "{hello}",
wantVars: map[string]int{
"hello": 0,
},
},
{
name: "with subgroup",
input: "{hello.123}",
wantVars: map[string]int{
"hello": 123,
},
},
{
name: "subgroup with spaces",
input: "{\thell0 . 123 }",
wantVars: map[string]int{
"hell0": 123,
},
},
{
name: "multiple groups",
input: "foo {bar} {bazz.buzz} {buzz.2}",
wantVars: map[string]int{
"bar": 0,
"buzz": 2,
},
},
{
name: "nested groups",
input: "{foo {bar}}",
wantVars: map[string]int{
"bar": 0,
},
},
{
name: "decimal without number",
input: "{foo.}",
wantVars: map[string]int{
"foo": 0,
},
},
{
name: "negative number",
input: "{foo.-1}",
wantVars: map[string]int{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewRegexVarString(tt.input)
assert.Equal(t, tt.input, got.original)
assert.Equal(t, tt.wantVars, got.variables)
})
}
}
================================================
FILE: pkg/custom_detectors/validation.go
================================================
package custom_detectors
import (
"fmt"
"regexp"
"strconv"
"strings"
)
func ValidateKeywords(keywords []string) error {
if len(keywords) == 0 {
return fmt.Errorf("no keywords")
}
for _, keyword := range keywords {
if len(keyword) == 0 {
return fmt.Errorf("empty keyword")
}
}
return nil
}
func ValidateRegex(regex map[string]string) error {
if len(regex) == 0 {
return fmt.Errorf("no regex")
}
for name, reg := range regex {
if _, err := regexp.Compile(reg); err != nil {
return fmt.Errorf("regex '%s': %w", name, err)
}
}
return nil
}
func ValidateRegexSlice(regex []string) error {
for i, reg := range regex {
if _, err := regexp.Compile(reg); err != nil {
return fmt.Errorf("regex '%d': %w", i+1, err)
}
}
return nil
}
// validates if a provided non-empty primary regex name exists in the map of regexes
func ValidatePrimaryRegexName(primaryRegexName string, regexes map[string]string) error {
if primaryRegexName == "" {
return nil
}
if _, ok := regexes[primaryRegexName]; !ok {
return fmt.Errorf("unknown primary regex name: %q", primaryRegexName)
}
return nil
}
func ValidateVerifyEndpoint(endpoint string, unsafe bool) error {
if len(endpoint) == 0 {
return fmt.Errorf("no endpoint")
}
if strings.HasPrefix(endpoint, "http://") && !unsafe {
return fmt.Errorf("http endpoint must have unsafe=true")
}
return nil
}
func ValidateVerifyHeaders(headers []string) error {
for _, header := range headers {
if !strings.Contains(header, ":") {
return fmt.Errorf("header %q must contain a colon", header)
}
}
return nil
}
func ValidateVerifyRanges(ranges []string) error {
const httpLowerRange = 100
const httpUpperRange = 599
for _, successRange := range ranges {
if !strings.Contains(successRange, "-") {
httpCode, err := strconv.Atoi(successRange)
if err != nil {
return fmt.Errorf("unable to convert http code to int %q", successRange)
}
if httpCode < httpLowerRange || httpCode > httpUpperRange {
return fmt.Errorf("invalid http status code %q", successRange)
}
continue
}
httpRange := strings.Split(successRange, "-")
if len(httpRange) != 2 {
return fmt.Errorf("invalid range format %q", successRange)
}
lowerBound, err := strconv.Atoi(httpRange[0])
if err != nil {
return fmt.Errorf("unable to convert lower bound to int %q", successRange)
}
upperBound, err := strconv.Atoi(httpRange[1])
if err != nil {
return fmt.Errorf("unable to convert upper bound to int %q", successRange)
}
if lowerBound > upperBound {
return fmt.Errorf("lower bound greater than upper bound on range %q", successRange)
}
if lowerBound < httpLowerRange || upperBound > httpUpperRange {
return fmt.Errorf("invalid http status code range %q", successRange)
}
}
return nil
}
func ValidateRegexVars(regex map[string]string, body ...string) error {
for _, b := range body {
matches := NewRegexVarString(b).variables
for match := range matches {
if _, ok := regex[match]; !ok {
return fmt.Errorf("body %q contains an unknown variable", b)
}
}
}
return nil
}
// === Custom Validations ===
// ContainsDigit checks if string contains at least one digit
func ContainsDigit(s string) bool {
for i := 0; i < len(s); i++ {
char := s[i]
if char >= '0' && char <= '9' {
return true
}
}
return false
}
// ContainsLowercase checks if string contains at least one lowercase letter
func ContainsLowercase(s string) bool {
for i := 0; i < len(s); i++ {
char := s[i]
if char >= 'a' && char <= 'z' {
return true
}
}
return false
}
// ContainsUppercase checks if string contains at least one uppercase letter
func ContainsUppercase(s string) bool {
for i := 0; i < len(s); i++ {
char := s[i]
if char >= 'A' && char <= 'Z' {
return true
}
}
return false
}
// ContainsSpecialChar checks if string contains at least one special character
func ContainsSpecialChar(s string) bool {
specialChars := "!@#$%^&*()_+-=[]{}|;:,.<>?"
return strings.ContainsAny(s, specialChars)
}
================================================
FILE: pkg/custom_detectors/validation_test.go
================================================
package custom_detectors
import (
"testing"
)
func TestCustomDetectorsKeywordValidation(t *testing.T) {
tests := []struct {
name string
input []string
wantErr bool
}{
{
name: "Test empty list of keywords",
input: []string{},
wantErr: true,
},
{
name: "Test empty keyword",
input: []string{""},
wantErr: true,
},
{
name: "Test valid keywords",
input: []string{"hello", "world"},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateKeywords(tt.input)
if (got != nil && !tt.wantErr) || (got == nil && tt.wantErr) {
t.Errorf("ValidateKeywords() error = %v, wantErr %v", got, tt.wantErr)
}
})
}
}
func TestCustomDetectorsRegexValidation(t *testing.T) {
tests := []struct {
name string
input map[string]string
wantErr bool
}{
{
name: "Test list of keywords",
input: map[string]string{
"id_pat_example": "([a-zA-Z0-9]{32})",
},
wantErr: false,
},
{
name: "Test empty list of keywords",
input: map[string]string{},
wantErr: true,
},
{
name: "Test invalid regex",
input: map[string]string{
"test": "!!?(?:?)[a-zA-Z0-9]{32}",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateRegex(tt.input)
if (got != nil && !tt.wantErr) || (got == nil && tt.wantErr) {
t.Errorf("ValidateRegex() error = %v, wantErr %v", got, tt.wantErr)
}
})
}
}
func TestCustomDetectorsVerifyEndpointValidation(t *testing.T) {
tests := []struct {
name string
endpoint string
unsafe bool
wantErr bool
}{
{
name: "Test http endpoint with unsafe flag",
endpoint: "http://localhost:8000/{id_pat_example}",
unsafe: true,
wantErr: false,
},
{
name: "Test http endpoint without unsafe flag",
endpoint: "http://localhost:8000/{id_pat_example}",
unsafe: false,
wantErr: true,
},
{
name: "Test https endpoint with unsafe flag",
endpoint: "https://localhost:8000/{id_pat_example}",
unsafe: true,
wantErr: false,
},
{
name: "Test https endpoint without unsafe flag",
endpoint: "https://localhost:8000/{id_pat_example}",
unsafe: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateVerifyEndpoint(tt.endpoint, tt.unsafe)
if (got != nil && !tt.wantErr) || (got == nil && tt.wantErr) {
t.Errorf("ValidateVerifyEndpoint() error = %v, wantErr %v", got, tt.wantErr)
}
})
}
}
func TestCustomDetectorsVerifyHeadersValidation(t *testing.T) {
tests := []struct {
name string
headers []string
wantErr bool
}{
{
name: "Test single header",
headers: []string{"Authorization: Bearer {secret_pat_example.0}"},
wantErr: false,
},
{
name: "Test invalid header",
headers: []string{"Hello world"},
wantErr: true,
},
{
name: "Test ugly header",
headers: []string{"Hello:::::::world::hi:"},
wantErr: false,
},
{
name: "Test empty header",
headers: []string{},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateVerifyHeaders(tt.headers)
if (got != nil && !tt.wantErr) || (got == nil && tt.wantErr) {
t.Errorf("ValidateVerifyHeaders() error = %v, wantErr %v", got, tt.wantErr)
}
})
}
}
func TestCustomDetectorsVerifyRangeValidation(t *testing.T) {
tests := []struct {
name string
ranges []string
wantErr bool
}{
{
name: "Test multiple mixed ranges",
ranges: []string{"200", "300-350"},
wantErr: false,
},
{
name: "Test invalid non-number range",
ranges: []string{"hi"},
wantErr: true,
},
{
name: "Test invalid lower to upper range",
ranges: []string{"200-100"},
wantErr: true,
},
{
name: "Test invalid http range",
ranges: []string{"400-1000"},
wantErr: true,
},
{
name: "Test multiple ranges with invalid inputs",
ranges: []string{"322", "hello-world", "100-200"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateVerifyRanges(tt.ranges)
if (got != nil && !tt.wantErr) || (got == nil && tt.wantErr) {
t.Errorf("ValidateVerifyRanges() error = %v, wantErr %v", got, tt.wantErr)
}
})
}
}
func TestCustomDetectorsVerifyRegexVarsValidation(t *testing.T) {
tests := []struct {
name string
regex map[string]string
body string
wantErr bool
}{
{
name: "Regex defined but not used in body",
regex: map[string]string{"id": "[0-9]{1,10}", "id_pat_example": "([a-zA-Z0-9]{32})"},
body: "hello world",
wantErr: false,
},
{
name: "Regex defined and is used in body",
regex: map[string]string{"id": "[0-9]{1,10}", "id_pat_example": "([a-zA-Z0-9]{32})"},
body: "hello world {id}",
wantErr: false,
},
{
name: "Regex var in body but not defined",
regex: map[string]string{"id": "[0-9]{1,10}", "id_pat_example": "([a-zA-Z0-9]{32})"},
body: "hello world {hello}",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateRegexVars(tt.regex, tt.body)
if (got != nil && !tt.wantErr) || (got == nil && tt.wantErr) {
t.Errorf("ValidateRegexVars() error = %v, wantErr %v", got, tt.wantErr)
}
})
}
}
func TestContainsDigit(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "contains digit",
args: args{s: "lzscqf&60M"},
want: true,
},
{
name: "does not contains digit",
args: args{s: "ZlDQOdaM*vsT"},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ContainsDigit(tt.args.s); got != tt.want {
t.Errorf("ContainsDigit() = %v, want %v", got, tt.want)
}
})
}
}
func TestContainsLowercase(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "contains lower case",
args: args{s: "g0AJBHdnhRG2"},
want: true,
},
{
name: "does not contains lower case",
args: args{s: "V7T#MEA6@+TN"},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ContainsLowercase(tt.args.s); got != tt.want {
t.Errorf("ContainsDigit() = %v, want %v", got, tt.want)
}
})
}
}
func TestContainsUppercase(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "contains upper case",
args: args{s: "G1sKkJeKlSQf"},
want: true,
},
{
name: "does not contains upper case",
args: args{s: "pq6-14ydz1@d"},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ContainsUppercase(tt.args.s); got != tt.want {
t.Errorf("ContainsDigit() = %v, want %v", got, tt.want)
}
})
}
}
func TestContainsSpecialChar(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "contains upper case",
args: args{s: "HP$gE7s=do0B"},
want: true,
},
{
name: "does not contains upper case",
args: args{s: "w9gvBYctrSjB"},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ContainsSpecialChar(tt.args.s); got != tt.want {
t.Errorf("ContainsDigit() = %v, want %v", got, tt.want)
}
})
}
}
================================================
FILE: pkg/decoders/base64.go
================================================
package decoders
import (
"bytes"
"encoding/base64"
"unicode"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
type (
Base64 struct{}
)
var (
b64Charset = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/-_=")
b64EndChars = "+/-_="
// Given characters are mostly ASCII, we can use a simple array to map.
b64CharsetMapping [128]bool
)
func init() {
// Build an array of all the characters in the base64 charset.
for _, char := range b64Charset {
b64CharsetMapping[char] = true
}
}
func (d *Base64) Type() detectorspb.DecoderType {
return detectorspb.DecoderType_BASE64
}
func (d *Base64) FromChunk(chunk *sources.Chunk) *DecodableChunk {
decodableChunk := &DecodableChunk{Chunk: chunk, DecoderType: d.Type()}
encodedSubstrings := getSubstringsOfCharacterSet(chunk.Data, 20, b64CharsetMapping, b64EndChars)
decodedSubstrings := make(map[string][]byte)
for _, str := range encodedSubstrings {
dec, err := base64.StdEncoding.DecodeString(str)
if err == nil && len(dec) > 0 && isASCII(dec) {
decodedSubstrings[str] = dec
}
dec, err = base64.RawURLEncoding.DecodeString(str)
if err == nil && len(dec) > 0 && isASCII(dec) {
decodedSubstrings[str] = dec
}
}
if len(decodedSubstrings) > 0 {
var result bytes.Buffer
result.Grow(len(chunk.Data))
start := 0
for _, encoded := range encodedSubstrings {
if decoded, ok := decodedSubstrings[encoded]; ok {
end := bytes.Index(chunk.Data[start:], []byte(encoded))
if end != -1 {
result.Write(chunk.Data[start : start+end])
result.Write(decoded)
start += end + len(encoded)
}
}
}
result.Write(chunk.Data[start:])
chunk.Data = result.Bytes()
return decodableChunk
}
return nil
}
func isASCII(b []byte) bool {
for i := 0; i < len(b); i++ {
if b[i] > unicode.MaxASCII {
return false
}
}
return true
}
func getSubstringsOfCharacterSet(data []byte, threshold int, charsetMapping [128]bool, endChars string) []string {
if len(data) == 0 {
return nil
}
count := 0
substringsCount := 0
// Determine the number of substrings that will be returned.
// Pre-allocate the slice to avoid reallocations.
for _, char := range data {
if char < 128 && charsetMapping[char] {
count++
} else {
if count > threshold {
substringsCount++
}
count = 0
}
}
if count > threshold {
substringsCount++
}
count = 0
start := 0
substrings := make([]string, 0, substringsCount)
for i, char := range data {
if char < 128 && charsetMapping[char] {
if count == 0 {
start = i
}
count++
} else {
if count > threshold {
substrings = appendB64Substring(data, start, count, substrings, endChars)
}
count = 0
}
}
if count > threshold {
substrings = appendB64Substring(data, start, count, substrings, endChars)
}
return substrings
}
func appendB64Substring(data []byte, start, count int, substrings []string, endChars string) []string {
substring := bytes.TrimLeft(data[start:start+count], endChars)
if idx := bytes.IndexByte(bytes.TrimRight(substring, endChars), '='); idx != -1 {
substrings = append(substrings, string(substring[idx+1:]))
} else {
substrings = append(substrings, string(substring))
}
return substrings
}
================================================
FILE: pkg/decoders/base64_test.go
================================================
package decoders
import (
"testing"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
func TestBase64_FromChunk(t *testing.T) {
tests := []struct {
chunk *sources.Chunk
want *sources.Chunk
name string
}{
{
name: "only b64 chunk",
chunk: &sources.Chunk{
Data: []byte(`bG9uZ2VyLWVuY29kZWQtc2VjcmV0LXRlc3Q=`),
},
want: &sources.Chunk{
Data: []byte(`longer-encoded-secret-test`),
},
},
{
name: "mixed content",
chunk: &sources.Chunk{
Data: []byte(`token: bG9uZ2VyLWVuY29kZWQtc2VjcmV0LXRlc3Q=`),
},
want: &sources.Chunk{
Data: []byte(`token: longer-encoded-secret-test`),
},
},
{
name: "no chunk",
chunk: &sources.Chunk{
Data: []byte(``),
},
want: nil,
},
{
name: "env var (looks like all b64 decodable but has `=` in the middle)",
chunk: &sources.Chunk{
Data: []byte(`some-encoded-secret=dGVzdHNlY3JldA==`),
},
want: &sources.Chunk{
Data: []byte(`some-encoded-secret=testsecret`),
},
},
{
name: "has longer b64 inside",
chunk: &sources.Chunk{
Data: []byte(`some-encoded-secret="bG9uZ2VyLWVuY29kZWQtc2VjcmV0LXRlc3Q="`),
},
want: &sources.Chunk{
Data: []byte(`some-encoded-secret="longer-encoded-secret-test"`),
},
},
{
name: "many possible substrings",
chunk: &sources.Chunk{
Data: []byte(`Many substrings in this slack message could be base64 decoded
but only dGhpcyBlbmNhcHN1bGF0ZWQgc2VjcmV0 should be decoded.`),
},
want: &sources.Chunk{
Data: []byte(`Many substrings in this slack message could be base64 decoded
but only this encapsulated secret should be decoded.`),
},
},
{
name: "b64-url-safe: only b64 chunk",
chunk: &sources.Chunk{
Data: []byte(`bG9uZ2VyLWVuY29kZWQtc2VjcmV0LXRlc3Q`),
},
want: &sources.Chunk{
Data: []byte(`longer-encoded-secret-test`),
},
},
{
name: "b64-url-safe: mixed content",
chunk: &sources.Chunk{
Data: []byte(`token: bG9uZ2VyLWVuY29kZWQtc2VjcmV0LXRlc3Q`),
},
want: &sources.Chunk{
Data: []byte(`token: longer-encoded-secret-test`),
},
},
{
name: "b64-url-safe: env var (looks like all b64 decodable but has `=` in the middle)",
chunk: &sources.Chunk{
Data: []byte(`some-encoded-secret=dGVzdHNlY3JldA`),
},
want: &sources.Chunk{
Data: []byte(`some-encoded-secret=testsecret`),
},
},
{
name: "b64-url-safe: has longer b64 inside",
chunk: &sources.Chunk{
Data: []byte(`some-encoded-secret="bG9uZ2VyLWVuY29kZWQtc2VjcmV0LXRlc3Q"`),
},
want: &sources.Chunk{
Data: []byte(`some-encoded-secret="longer-encoded-secret-test"`),
},
},
{
name: "b64-url-safe: hyphen url b64",
chunk: &sources.Chunk{
Data: []byte(`dHJ1ZmZsZWhvZz4-ZmluZHMtc2VjcmV0cw`),
},
want: &sources.Chunk{
Data: []byte(`trufflehog>>finds-secrets`),
},
},
{
name: "b64-url-safe: underscore url b64",
chunk: &sources.Chunk{
Data: []byte(`YjY0dXJsc2FmZS10ZXN0LXNlY3JldC11bmRlcnNjb3Jlcz8_`),
},
want: &sources.Chunk{
Data: []byte(`b64urlsafe-test-secret-underscores??`),
},
},
{
name: "invalid base64 string",
chunk: &sources.Chunk{
Data: []byte(`a3d3fa7c2bb99e469ba55e5834ce79ee4853a8a3`),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &Base64{}
got := d.FromChunk(tt.chunk)
if tt.want != nil {
if got == nil {
t.Fatal("got nil, did not want nil")
}
if diff := pretty.Compare(string(got.Data), string(tt.want.Data)); diff != "" {
t.Errorf("Base64FromChunk() %s diff: (-got +want)\n%s", tt.name, diff)
}
} else {
if got != nil {
t.Error("Expected nil chunk")
}
}
})
}
}
func BenchmarkFromChunkSmall(b *testing.B) {
d := Base64{}
data := detectors.MustGetBenchmarkData()["small"]
for b.Loop() {
d.FromChunk(&sources.Chunk{Data: data})
}
}
func BenchmarkFromChunkMedium(b *testing.B) {
d := Base64{}
data := detectors.MustGetBenchmarkData()["medium"]
for b.Loop() {
d.FromChunk(&sources.Chunk{Data: data})
}
}
func BenchmarkFromChunkLarge(b *testing.B) {
d := Base64{}
data := detectors.MustGetBenchmarkData()["big"]
for b.Loop() {
d.FromChunk(&sources.Chunk{Data: data})
}
}
================================================
FILE: pkg/decoders/decoders.go
================================================
package decoders
import (
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
func DefaultDecoders() []Decoder {
return []Decoder{
// UTF8 must be first for duplicate detection
&UTF8{},
&Base64{},
&UTF16{},
&EscapedUnicode{},
}
}
// DecodableChunk is a chunk that includes the type of decoder used.
// This allows us to avoid a type assertion on each decoder.
type DecodableChunk struct {
*sources.Chunk
DecoderType detectorspb.DecoderType
}
type Decoder interface {
FromChunk(chunk *sources.Chunk) *DecodableChunk
Type() detectorspb.DecoderType
}
// Fuzz is an entrypoint for go-fuzz, which is an AFL-style fuzzing tool.
// This one attempts to uncover any panics during decoding.
func Fuzz(data []byte) int {
decoded := false
for i, decoder := range DefaultDecoders() {
// Skip the first decoder (plain), because it will always decode and give
// priority to the input (return 1).
if i == 0 {
continue
}
chunk := decoder.FromChunk(&sources.Chunk{Data: data})
if chunk != nil {
decoded = true
}
}
if decoded {
return 1 // prioritize the input
}
return -1 // Don't add input to the corpus.
}
================================================
FILE: pkg/decoders/escaped_unicode.go
================================================
package decoders
import (
"bytes"
"regexp"
"strconv"
"unicode/utf8"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
type EscapedUnicode struct{}
var _ Decoder = (*EscapedUnicode)(nil)
// It might be advantageous to limit these to a subset of acceptable characters, similar to base64.
// https://dencode.com/en/string/unicode-escape
var (
// Standard Unicode notation.
//https://unicode.org/standard/principles.html
codePointPat = regexp.MustCompile(`\bU\+([a-fA-F0-9]{4}).?`)
// Common escape sequence used in programming languages.
escapePat = regexp.MustCompile(`(?i:\\{1,2}u)([a-fA-F0-9]{4})`)
// Additional Unicode escape formats from dencode.com
// \u{X} format - Rust, Swift, some JS, etc. (variable length hex in braces)
braceEscapePat = regexp.MustCompile(`\\u\{([a-fA-F0-9]{1,6})\}`)
// \U00XXXXXX format - Python, etc. (8-digit format for non-BMP characters)
longEscapePat = regexp.MustCompile(`\\U([a-fA-F0-9]{8})`)
// \x{X} format - Perl (variable length hex in braces)
perlEscapePat = regexp.MustCompile(`\\x\{([a-fA-F0-9]{1,6})\}`)
// \X format - CSS (hex without padding). Go's regexp (RE2) has no look-ahead, so we
// include the delimiter (whitespace, another backslash, or end-of-string) in the
// match using a non-capturing group. The delimiter is later re-inserted by the
// decoder when necessary.
cssEscapePat = regexp.MustCompile(`\\([a-fA-F0-9]{1,6})(?:\s|\\|$)`)
// X; format - HTML/XML (hex with semicolon)
htmlEscapePat = regexp.MustCompile(`([a-fA-F0-9]{1,6});`)
// %uXXXX format - Percent-encoding (non-standard)
percentEscapePat = regexp.MustCompile(`%u([a-fA-F0-9]{4})`)
// // 0xX format - Hexadecimal notation with space separation
// Note: Commenting out for now due to high memory overhead. Review ways to handle this.
// hexEscapePat = regexp.MustCompile(`0x([a-fA-F0-9]{1,6})(?:\s|$)`)
)
func (d *EscapedUnicode) Type() detectorspb.DecoderType {
return detectorspb.DecoderType_ESCAPED_UNICODE
}
func (d *EscapedUnicode) FromChunk(chunk *sources.Chunk) *DecodableChunk {
if chunk == nil || len(chunk.Data) == 0 {
return nil
}
var (
// Necessary to avoid data races.
chunkData = bytes.Clone(chunk.Data)
matched = false
)
// Process patterns in priority order - more specific patterns first
// This prevents conflicts where multiple patterns match the same input
// Long escape format (8 hex digits) - highest priority
if longEscapePat.Match(chunkData) {
matched = true
chunkData = decodeLongEscape(chunkData)
} else if braceEscapePat.Match(chunkData) {
matched = true
chunkData = decodeBraceEscape(chunkData)
} else if perlEscapePat.Match(chunkData) {
matched = true
chunkData = decodePerlEscape(chunkData)
} else if htmlEscapePat.Match(chunkData) {
matched = true
chunkData = decodeHtmlEscape(chunkData)
} else if percentEscapePat.Match(chunkData) {
matched = true
chunkData = decodePercentEscape(chunkData)
} else if escapePat.Match(chunkData) {
matched = true
chunkData = decodeEscaped(chunkData)
} else if codePointPat.Match(chunkData) {
matched = true
chunkData = decodeCodePoint(chunkData)
} else if cssEscapePat.Match(chunkData) {
matched = true
chunkData = decodeCssEscape(chunkData)
// } else if hexEscapePat.Match(chunkData) {
// matched = true
// chunkData = decodeHexEscape(chunkData)
}
if matched {
return &DecodableChunk{
DecoderType: d.Type(),
Chunk: &sources.Chunk{
Data: chunkData,
OriginalData: chunk.OriginalData,
SourceName: chunk.SourceName,
SourceID: chunk.SourceID,
JobID: chunk.JobID,
SecretID: chunk.SecretID,
SourceMetadata: chunk.SourceMetadata,
SourceType: chunk.SourceType,
SourceVerify: chunk.SourceVerify,
},
}
} else {
return nil
}
}
// Unicode characters are encoded as 1 to 4 bytes per rune.
const maxBytesPerRune = 4
const spaceChar = byte(' ')
// decodeWithPattern replaces escape sequences matched by re with their UTF-8
// equivalents. The regex *must* have the first capturing group contain the
// hexadecimal code-point digits. Any invalid value (> 0x10FFFF or parse error)
// is skipped. The replacement walks matches in reverse order to avoid index
// shifts.
func decodeWithPattern(input []byte, re *regexp.Regexp) []byte {
indices := re.FindAllSubmatchIndex(input, -1)
if len(indices) == 0 {
return input
}
utf8Bytes := make([]byte, maxBytesPerRune)
for i := len(indices) - 1; i >= 0; i-- {
m := indices[i]
start, end := m[0], m[1]
hexStart, hexEnd := m[2], m[3]
cp, err := strconv.ParseUint(string(input[hexStart:hexEnd]), 16, 32)
if err != nil || cp > 0x10FFFF {
continue
}
utf8Len := utf8.EncodeRune(utf8Bytes, rune(cp))
input = append(input[:start], append(utf8Bytes[:utf8Len], input[end:]...)...)
}
return input
}
func decodeCodePoint(input []byte) []byte {
// Find all Unicode escape sequences in the input byte slice
indices := codePointPat.FindAllSubmatchIndex(input, -1)
// Iterate over found indices in reverse order to avoid modifying the slice length
utf8Bytes := make([]byte, maxBytesPerRune)
for i := len(indices) - 1; i >= 0; i-- {
matches := indices[i]
startIndex := matches[0]
endIndex := matches[1]
hexStartIndex := matches[2]
hexEndIndex := matches[3]
// If the input is like `U+1234 U+5678` we should replace `U+1234 `.
// Otherwise, we should only replace `U+1234`.
if endIndex != hexEndIndex && input[endIndex-1] != spaceChar {
endIndex = endIndex - 1
}
// Extract the hexadecimal value from the escape sequence
hexValue := string(input[hexStartIndex:hexEndIndex])
// Parse the hexadecimal value to an integer
unicodeInt, err := strconv.ParseInt(hexValue, 16, 32)
if err != nil {
// If there's an error, continue to the next escape sequence
continue
}
// Convert the Unicode code point to a UTF-8 representation
utf8Len := utf8.EncodeRune(utf8Bytes, rune(unicodeInt))
// Replace the escape sequence with the UTF-8 representation
input = append(input[:startIndex], append(utf8Bytes[:utf8Len], input[endIndex:]...)...)
}
return input
}
func decodeEscaped(input []byte) []byte {
return decodeWithPattern(input, escapePat)
}
// decodeBraceEscape handles \u{X} format - Rust, Swift, some JS, etc.
func decodeBraceEscape(input []byte) []byte {
return decodeWithPattern(input, braceEscapePat)
}
// decodeLongEscape handles \U00XXXXXX format - Python, etc.
func decodeLongEscape(input []byte) []byte {
return decodeWithPattern(input, longEscapePat)
}
// decodePerlEscape handles \x{X} format - Perl
func decodePerlEscape(input []byte) []byte {
return decodeWithPattern(input, perlEscapePat)
}
// decodeCssEscape handles \X format - CSS (hex without padding, with space delimiter or end of string or next hex sequence)
func decodeCssEscape(input []byte) []byte {
return decodeWithPattern(input, cssEscapePat)
}
// decodeHtmlEscape handles X; format - HTML/XML
func decodeHtmlEscape(input []byte) []byte {
return decodeWithPattern(input, htmlEscapePat)
}
// decodePercentEscape handles %uXXXX format - Percent-encoding (non-standard)
func decodePercentEscape(input []byte) []byte {
return decodeWithPattern(input, percentEscapePat)
}
// decodeHexEscape handles 0xX format - Hexadecimal notation with space separation
// func decodeHexEscape(input []byte) []byte {
// // This format requires consecutive 0xNN sequences to be considered for decoding
// // We'll look for patterns of multiple consecutive hex values
// hexPattern := regexp.MustCompile(`(?:0x[a-fA-F0-9]{1,2}(?:\s+|$))+`)
// matches := hexPattern.FindAll(input, -1)
// if len(matches) == 0 {
// return input
// }
// result := input
// for _, match := range matches {
// // Extract individual hex values
// individualHex := regexp.MustCompile(`0x([a-fA-F0-9]{1,2})`)
// hexMatches := individualHex.FindAllSubmatch(match, -1)
// // Only decode if we have multiple consecutive hex values (likely to be a Unicode string)
// if len(hexMatches) < 3 {
// continue
// }
// var decoded []byte
// for _, hexMatch := range hexMatches {
// hexValue := string(hexMatch[1])
// if len(hexValue) == 1 {
// hexValue = "0" + hexValue // Pad single digit hex values
// }
// unicodeInt, err := strconv.ParseUint(hexValue, 16, 32)
// if err != nil || unicodeInt > 0x10FFFF {
// break
// }
// if unicodeInt <= 0x7F {
// // ASCII character
// decoded = append(decoded, byte(unicodeInt))
// } else {
// // Unicode character
// utf8Bytes := make([]byte, maxBytesPerRune)
// utf8Len := utf8.EncodeRune(utf8Bytes, rune(unicodeInt))
// decoded = append(decoded, utf8Bytes[:utf8Len]...)
// }
// }
// // Replace the original sequence with decoded bytes
// result = bytes.Replace(result, match, decoded, 1)
// }
// return result
// }
================================================
FILE: pkg/decoders/escaped_unicode_bench_test.go
================================================
package decoders
import (
"testing"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
// Benchmark data for testing
var (
// Original formats
originalUnicodeData = []byte("\\u0041\\u004b\\u0049\\u0041\\u0055\\u004d\\u0034\\u0047\\u0036\\u004f\\u0036\\u004e\\u0041\\u004b\\u0045\\u0037\\u004c\\u0043\\u0044\\u004a")
codePointData = []byte("U+0041 U+004B U+0049 U+0041 U+0055 U+004D U+0034 U+0047 U+0036 U+004F U+0036 U+004E U+0041 U+004B U+0045 U+0037 U+004C U+0043 U+0044 U+004A")
// New formats
braceEscapeData = []byte("\\u{41}\\u{4b}\\u{49}\\u{41}\\u{55}\\u{4d}\\u{34}\\u{47}\\u{36}\\u{4f}\\u{36}\\u{4e}\\u{41}\\u{4b}\\u{45}\\u{37}\\u{4c}\\u{43}\\u{44}\\u{4a}")
longEscapeData = []byte("\\U00000041\\U0000004b\\U00000049\\U00000041\\U00000055\\U0000004d\\U00000034\\U00000047\\U00000036\\U0000004f\\U00000036\\U0000004e\\U00000041\\U0000004b\\U00000045\\U00000037\\U0000004c\\U00000043\\U00000044\\U0000004a")
perlEscapeData = []byte("\\x{41}\\x{4b}\\x{49}\\x{41}\\x{55}\\x{4d}\\x{34}\\x{47}\\x{36}\\x{4f}\\x{36}\\x{4e}\\x{41}\\x{4b}\\x{45}\\x{37}\\x{4c}\\x{43}\\x{44}\\x{4a}")
cssEscapeData = []byte("\\41 \\4b \\49 \\41 \\55 \\4d \\34 \\47 \\36 \\4f \\36 \\4e \\41 \\4b \\45 \\37 \\4c \\43 \\44 \\4a ")
htmlEscapeData = []byte("AKIAUM4G6O6NAKE7LCDJ")
percentEscapeData = []byte("%u0041%u004b%u0049%u0041%u0055%u004d%u0034%u0047%u0036%u004f%u0036%u004e%u0041%u004b%u0045%u0037%u004c%u0043%u0044%u004a")
//hexEscapeData = []byte("0x41 0x4b 0x49 0x41 0x55 0x4d 0x34 0x47 0x36 0x4f 0x36 0x4e 0x41 0x4b 0x45 0x37 0x4c 0x43 0x44 0x4a ")
// Mixed content (more realistic scenario)
mixedContentData = []byte(`
const config = {
apiKey: "\\u0041\\u004b\\u0049\\u0041\\u0055\\u004d\\u0034\\u0047\\u0036\\u004f\\u0036\\u004e\\u0041\\u004b\\u0045\\u0037\\u004c\\u0043\\u0044\\u004a",
secretKey: "\\u{6e}\\u{62}\\u{75}\\u{68}\\u{7a}\\u{4b}\\u{79}\\u{39}\\u{50}\\u{50}\\u{7a}\\u{32}\\u{7a}\\u{47}\\u{33}\\u{47}\\u{54}\\u{4a}\\u{71}\\u{4b}\\u{45}\\u{43}\\u{6e}\\u{71}\\u{4c}\\u{41}\\u{78}\\u{43}\\u{76}\\u{2f}\\u{36}\\u{68}\\u{43}\\u{6a}\\u{6b}\\u{50}\\u{68}\\u{66}\\u{58}\\u{6f}",
htmlToken: "AKIAUM4G6O6NAKE7LCDJ",
normalText: "This is normal text that should not be processed"
}
`)
// Large data for stress testing
largeData = func() []byte {
data := make([]byte, 0, 10000)
for i := 0; i < 100; i++ {
data = append(data, originalUnicodeData...)
data = append(data, braceEscapeData...)
data = append(data, longEscapeData...)
data = append(data, htmlEscapeData...)
data = append(data, []byte(" normal text ")...)
}
return data
}()
// No Unicode data (worst case for performance)
noUnicodeData = []byte(`
This is a large block of text with no Unicode escape sequences.
It contains various programming constructs like:
- Variable declarations: var x = 123;
- Function calls: doSomething(param1, param2);
- Comments: /* this is a comment */
- Strings: "hello world"
- Numbers: 42, 3.14159, 0xFF
- But no Unicode escapes that would trigger our decoders.
This simulates the common case where files don't contain Unicode escapes.
`)
)
// Benchmark individual decoder functions
func BenchmarkDecodeOriginalEscape(b *testing.B) {
for b.Loop() {
_ = decodeEscaped(originalUnicodeData)
}
}
func BenchmarkDecodeCodePoint(b *testing.B) {
for b.Loop() {
_ = decodeCodePoint(codePointData)
}
}
func BenchmarkDecodeBraceEscape(b *testing.B) {
for b.Loop() {
_ = decodeBraceEscape(braceEscapeData)
}
}
func BenchmarkDecodeLongEscape(b *testing.B) {
for b.Loop() {
_ = decodeLongEscape(longEscapeData)
}
}
func BenchmarkDecodePerlEscape(b *testing.B) {
for b.Loop() {
_ = decodePerlEscape(perlEscapeData)
}
}
func BenchmarkDecodeCssEscape(b *testing.B) {
for b.Loop() {
_ = decodeCssEscape(cssEscapeData)
}
}
func BenchmarkDecodeHtmlEscape(b *testing.B) {
for b.Loop() {
_ = decodeHtmlEscape(htmlEscapeData)
}
}
func BenchmarkDecodePercentEscape(b *testing.B) {
for b.Loop() {
_ = decodePercentEscape(percentEscapeData)
}
}
// func BenchmarkDecodeHexEscape(b *testing.B) {
// for i := 0; i < b.N; i++ {
// _ = decodeHexEscape(hexEscapeData)
// }
// }
// Benchmark the full FromChunk method with different data types
func BenchmarkFromChunk_OriginalFormat(b *testing.B) {
decoder := &EscapedUnicode{}
chunk := &sources.Chunk{Data: originalUnicodeData}
for b.Loop() {
_ = decoder.FromChunk(chunk)
}
}
func BenchmarkFromChunk_BraceFormat(b *testing.B) {
decoder := &EscapedUnicode{}
chunk := &sources.Chunk{Data: braceEscapeData}
for b.Loop() {
_ = decoder.FromChunk(chunk)
}
}
func BenchmarkFromChunk_LongFormat(b *testing.B) {
decoder := &EscapedUnicode{}
chunk := &sources.Chunk{Data: longEscapeData}
for b.Loop() {
_ = decoder.FromChunk(chunk)
}
}
func BenchmarkFromChunk_HtmlFormat(b *testing.B) {
decoder := &EscapedUnicode{}
chunk := &sources.Chunk{Data: htmlEscapeData}
for b.Loop() {
_ = decoder.FromChunk(chunk)
}
}
func BenchmarkFromChunk_MixedContent(b *testing.B) {
decoder := &EscapedUnicode{}
chunk := &sources.Chunk{Data: mixedContentData}
for b.Loop() {
_ = decoder.FromChunk(chunk)
}
}
func BenchmarkFromChunk_NoUnicode(b *testing.B) {
decoder := &EscapedUnicode{}
chunk := &sources.Chunk{Data: noUnicodeData}
for b.Loop() {
_ = decoder.FromChunk(chunk)
}
}
func BenchmarkFromChunk_LargeData(b *testing.B) {
decoder := &EscapedUnicode{}
chunk := &sources.Chunk{Data: largeData}
for b.Loop() {
_ = decoder.FromChunk(chunk)
}
}
// Benchmark regex matching performance (most expensive operation)
func BenchmarkRegexMatching_AllPatterns(b *testing.B) {
testData := mixedContentData
for b.Loop() {
// Simulate the pattern matching in FromChunk
_ = longEscapePat.Match(testData)
_ = braceEscapePat.Match(testData)
_ = perlEscapePat.Match(testData)
_ = htmlEscapePat.Match(testData)
_ = percentEscapePat.Match(testData)
_ = escapePat.Match(testData)
_ = codePointPat.Match(testData)
_ = cssEscapePat.Match(testData)
//_ = hexEscapePat.Match(testData)
}
}
func BenchmarkRegexMatching_NoMatch(b *testing.B) {
testData := noUnicodeData
for b.Loop() {
// Simulate the pattern matching in FromChunk on data with no matches
_ = longEscapePat.Match(testData)
_ = braceEscapePat.Match(testData)
_ = perlEscapePat.Match(testData)
_ = htmlEscapePat.Match(testData)
_ = percentEscapePat.Match(testData)
_ = escapePat.Match(testData)
_ = codePointPat.Match(testData)
_ = cssEscapePat.Match(testData)
//_ = hexEscapePat.Match(testData)
}
}
// Memory allocation benchmarks
func BenchmarkFromChunk_MemoryAllocation(b *testing.B) {
decoder := &EscapedUnicode{}
chunk := &sources.Chunk{Data: mixedContentData}
b.ReportAllocs()
for b.Loop() {
result := decoder.FromChunk(chunk)
if result != nil {
// Prevent compiler optimization
_ = result.Data
}
}
}
================================================
FILE: pkg/decoders/escaped_unicode_test.go
================================================
package decoders
import (
"testing"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
func TestUnicodeEscape_FromChunk(t *testing.T) {
tests := []struct {
name string
chunk *sources.Chunk
want *sources.Chunk
wantErr bool
}{
// U+1234
{
name: "[notation] all escaped",
chunk: &sources.Chunk{
Data: []byte("U+0074 U+006f U+006b U+0065 U+006e U+003a U+0020 U+0022 U+0067 U+0068 U+0070 U+005f U+0049 U+0077 U+0064 U+004d U+0078 U+0039 U+0057 U+0046 U+0057 U+0052 U+0052 U+0066 U+004d U+0068 U+0054 U+0059 U+0069 U+0061 U+0056 U+006a U+005a U+0037 U+0038 U+004a U+0066 U+0075 U+0061 U+006d U+0076 U+006e U+0030 U+0059 U+0057 U+0052 U+004d U+0030 U+0022"),
},
want: &sources.Chunk{
Data: []byte("token: \"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\""),
},
},
// \u1234
{
name: "[slash] all escaped",
chunk: &sources.Chunk{
Data: []byte("\\u0074\\u006f\\u006b\\u0065\\u006e\\u003a\\u0020\\u0022\\u0067\\u0068\\u0070\\u005f\\u0049\\u0077\\u0064\\u004d\\u0078\\u0039\\u0057\\u0046\\u0057\\u0052\\u0052\\u0066\\u004d\\u0068\\u0054\\u0059\\u0069\\u0061\\u0056\\u006a\\u005a\\u0037\\u0038\\u004a\\u0066\\u0075\\u0061\\u006d\\u0076\\u006e\\u0030\\u0059\\u0057\\u0052\\u004d\\u0030\\u0022"),
},
want: &sources.Chunk{
Data: []byte("token: \"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\""),
},
},
{
name: "[slash] mixed content",
chunk: &sources.Chunk{
Data: []byte("npm config set @trufflesec:registry=https://npm.pkg.github.com\nnpm config set //npm.pkg.github.com:_authToken=$'\\u0067hp_9ovSHEBCq0drG42yjoam76iNybtqLN25CgSf'"),
},
want: &sources.Chunk{
Data: []byte("npm config set @trufflesec:registry=https://npm.pkg.github.com\nnpm config set //npm.pkg.github.com:_authToken=$'ghp_9ovSHEBCq0drG42yjoam76iNybtqLN25CgSf'"),
},
},
{
name: "[slash] multiple slashes",
chunk: &sources.Chunk{
Data: []byte(`SameValue("hello","\\u0068el\\u006co"); // true`),
},
want: &sources.Chunk{
Data: []byte(`SameValue("hello","hello"); // true`),
},
},
// New test cases for additional Unicode escape formats
// \u{X} format - Rust, Swift, some JS, etc.
{
name: "[brace] \\u{X} format - Rust/Swift style",
chunk: &sources.Chunk{
Data: []byte("\\u{74}\\u{6f}\\u{6b}\\u{65}\\u{6e}\\u{3a}\\u{20}\\u{22}\\u{67}\\u{68}\\u{70}\\u{5f}\\u{49}\\u{77}\\u{64}\\u{4d}\\u{78}\\u{39}\\u{57}\\u{46}\\u{57}\\u{52}\\u{52}\\u{66}\\u{4d}\\u{68}\\u{54}\\u{59}\\u{69}\\u{61}\\u{56}\\u{6a}\\u{5a}\\u{37}\\u{38}\\u{4a}\\u{66}\\u{75}\\u{61}\\u{6d}\\u{76}\\u{6e}\\u{30}\\u{59}\\u{57}\\u{52}\\u{4d}\\u{30}\\u{22}"),
},
want: &sources.Chunk{
Data: []byte("token: \"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\""),
},
},
// \U00XXXXXX format - Python, etc.
{
name: "[long] \\U00XXXXXX format - Python style",
chunk: &sources.Chunk{
Data: []byte("\\U00000074\\U0000006f\\U0000006b\\U00000065\\U0000006e\\U0000003a\\U00000020\\U00000022\\U00000067\\U00000068\\U00000070\\U0000005f\\U00000049\\U00000077\\U00000064\\U0000004d\\U00000078\\U00000039\\U00000057\\U00000046\\U00000057\\U00000052\\U00000052\\U00000066\\U0000004d\\U00000068\\U00000054\\U00000059\\U00000069\\U00000061\\U00000056\\U0000006a\\U0000005a\\U00000037\\U00000038\\U0000004a\\U00000066\\U00000075\\U00000061\\U0000006d\\U00000076\\U0000006e\\U00000030\\U00000059\\U00000057\\U00000052\\U0000004d\\U00000030\\U00000022"),
},
want: &sources.Chunk{
Data: []byte("token: \"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\""),
},
},
// \x{X} format - Perl
{
name: "[perl] \\x{X} format - Perl style",
chunk: &sources.Chunk{
Data: []byte("\\x{74}\\x{6f}\\x{6b}\\x{65}\\x{6e}\\x{3a}\\x{20}\\x{22}\\x{67}\\x{68}\\x{70}\\x{5f}\\x{49}\\x{77}\\x{64}\\x{4d}\\x{78}\\x{39}\\x{57}\\x{46}\\x{57}\\x{52}\\x{52}\\x{66}\\x{4d}\\x{68}\\x{54}\\x{59}\\x{69}\\x{61}\\x{56}\\x{6a}\\x{5a}\\x{37}\\x{38}\\x{4a}\\x{66}\\x{75}\\x{61}\\x{6d}\\x{76}\\x{6e}\\x{30}\\x{59}\\x{57}\\x{52}\\x{4d}\\x{30}\\x{22}"),
},
want: &sources.Chunk{
Data: []byte("token: \"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\""),
},
},
// \X format - CSS (space delimited)
// ToDo: Look into supporting CSS where there is no whitespace ex: \013322\013171\013001. Currently not supported by this implementation.
{
name: "[css] \\X format - CSS style",
chunk: &sources.Chunk{
Data: []byte("\\74 \\6f \\6b \\65 \\6e \\3a \\20 \\22 \\67 \\68 \\70 \\5f \\49 \\77 \\64 \\4d \\78 \\39 \\57 \\46 \\57 \\52 \\52 \\66 \\4d \\68 \\54 \\59 \\69 \\61 \\56 \\6a \\5a \\37 \\38 \\4a \\66 \\75 \\61 \\6d \\76 \\6e \\30 \\59 \\57 \\52 \\4d \\30 \\22 "),
},
want: &sources.Chunk{
Data: []byte("token: \"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\""),
},
},
// X; format - HTML/XML
{
name: "[html] X; format - HTML/XML style",
chunk: &sources.Chunk{
Data: []byte("token: "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""),
},
want: &sources.Chunk{
Data: []byte("token: \"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\""),
},
},
// %uXXXX format - Percent-encoding (non-standard)
{
name: "[percent] %uXXXX format - Percent encoding",
chunk: &sources.Chunk{
Data: []byte("%u0074%u006f%u006b%u0065%u006e%u003a%u0020%u0022%u0067%u0068%u0070%u005f%u0049%u0077%u0064%u004d%u0078%u0039%u0057%u0046%u0057%u0052%u0052%u0066%u004d%u0068%u0054%u0059%u0069%u0061%u0056%u006a%u005a%u0037%u0038%u004a%u0066%u0075%u0061%u006d%u0076%u006e%u0030%u0059%u0057%u0052%u004d%u0030%u0022"),
},
want: &sources.Chunk{
Data: []byte("token: \"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\""),
},
},
// // 0xX format - Hexadecimal notation with space separation
// {
// name: "[hex] 0xX format - Hex with spaces",
// chunk: &sources.Chunk{
// Data: []byte("0x74 0x6f 0x6b 0x65 0x6e 0x3a 0x20 0x22 0x67 0x68 0x70 0x5f 0x49 0x77 0x64 0x4d 0x78 0x39 0x57 0x46 0x57 0x52 0x52 0x66 0x4d 0x68 0x54 0x59 0x69 0x61 0x56 0x6a 0x5a 0x37 0x38 0x4a 0x66 0x75 0x61 0x6d 0x76 0x6e 0x30 0x59 0x57 0x52 0x4d 0x30 0x22 "),
// },
// want: &sources.Chunk{
// Data: []byte("token: \"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\""),
// },
// },
// // 0xX format - Hexadecimal notation with comma separation
// {
// name: "[hex] 0xX format - Hex with commas",
// chunk: &sources.Chunk{
// Data: []byte("0x74,0x6f,0x6b,0x65,0x6e,0x3a,0x20,0x22,0x67,0x68,0x70,0x5f,0x49,0x77,0x64,0x4d,0x78,0x39,0x57,0x46,0x57,0x52,0x52,0x66,0x4d,0x68,0x54,0x59,0x69,0x61,0x56,0x6a,0x5a,0x37,0x38,0x4a,0x66,0x75,0x61,0x6d,0x76,0x6e,0x30,0x59,0x57,0x52,0x4d,0x30,0x22"),
// },
// want: &sources.Chunk{
// Data: []byte("token: \"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\""),
// },
// },
// Test cases for mixed content with new formats
{
name: "[mixed] \\u{X} in code context",
chunk: &sources.Chunk{
Data: []byte("const secret = \"\\u{41}\\u{4b}\\u{49}\\u{41}\\u{55}\\u{4d}\\u{34}\\u{47}\\u{36}\\u{4f}\\u{36}\\u{4e}\\u{41}\\u{4b}\\u{45}\\u{37}\\u{4c}\\u{43}\\u{44}\\u{4a}\";"),
},
want: &sources.Chunk{
Data: []byte("const secret = \"AKIAUM4G6O6NAKE7LCDJ\";"),
},
},
{
name: "[mixed] HTML entity in web context",
chunk: &sources.Chunk{
Data: []byte("AWS Key: AKIAUM4G6O6NAKE7LCDJ"),
},
want: &sources.Chunk{
Data: []byte("AWS Key: AKIAUM4G6O6NAKE7LCDJ"),
},
},
// Test cases for higher Unicode values (non-BMP)
{
name: "[emoji] \\u{X} with emoji",
chunk: &sources.Chunk{
Data: []byte("\\u{1f600} Happy face emoji"),
},
want: &sources.Chunk{
Data: []byte("😀 Happy face emoji"),
},
},
{
name: "[emoji] \\U00XXXXXX with emoji",
chunk: &sources.Chunk{
Data: []byte("\\U0001f600 Happy face emoji"),
},
want: &sources.Chunk{
Data: []byte("😀 Happy face emoji"),
},
},
// nothing
{
name: "no escaped",
chunk: &sources.Chunk{
Data: []byte(`-//npm.fontawesome.com/:_authToken=12345678-2323-1111-1111-12345670B312
+//npm.fontawesome.com/:_authToken=REMOVED_TOKEN`),
},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &EscapedUnicode{}
got := d.FromChunk(tt.chunk)
if tt.want != nil {
if got == nil {
t.Fatal("got nil, did not want nil")
}
if diff := pretty.Compare(string(tt.want.Data), string(got.Data)); diff != "" {
t.Errorf("UnicodeEscape.FromChunk() %s diff: (-want +got)\n%s", tt.name, diff)
}
} else {
if got != nil {
t.Error("Expected nil chunk")
}
}
})
}
}
================================================
FILE: pkg/decoders/utf16.go
================================================
package decoders
import (
"bytes"
"encoding/binary"
"unicode/utf8"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
type UTF16 struct{}
func (d *UTF16) Type() detectorspb.DecoderType {
return detectorspb.DecoderType_UTF16
}
func (d *UTF16) FromChunk(chunk *sources.Chunk) *DecodableChunk {
if chunk == nil || len(chunk.Data) == 0 {
return nil
}
decodableChunk := &DecodableChunk{Chunk: chunk, DecoderType: d.Type()}
if utf16Data, err := utf16ToUTF8(chunk.Data); err == nil {
if len(utf16Data) == 0 {
return nil
}
chunk.Data = utf16Data
return decodableChunk
}
return nil
}
// utf16ToUTF8 converts a byte slice containing UTF-16 encoded data to a UTF-8 encoded byte slice.
func utf16ToUTF8(b []byte) ([]byte, error) {
var bufBE, bufLE bytes.Buffer
for i := 0; i < len(b)-1; i += 2 {
if r := rune(binary.BigEndian.Uint16(b[i:])); b[i] == 0 && utf8.ValidRune(r) {
if isPrintableByte(byte(r)) {
bufBE.WriteRune(r)
}
}
if r := rune(binary.LittleEndian.Uint16(b[i:])); b[i+1] == 0 && utf8.ValidRune(r) {
if isPrintableByte(byte(r)) {
bufLE.WriteRune(r)
}
}
}
return append(bufLE.Bytes(), bufBE.Bytes()...), nil
}
================================================
FILE: pkg/decoders/utf16_test.go
================================================
package decoders
import (
"bytes"
"os"
"testing"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
func TestUTF16Decoder(t *testing.T) {
testCases := []struct {
name string
input []byte
expected []byte
expectNil bool
}{
{
name: "Valid UTF-16LE input",
input: []byte{72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100, 0},
expected: []byte("Hello World"),
expectNil: false,
},
{
name: "Valid UTF-16BE input",
input: []byte{0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100},
expected: []byte("Hello World"),
expectNil: false,
},
{
name: "Valid UTF-16LE input with BOM (FF FE)",
input: []byte{255, 254, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100, 0},
expected: []byte("Hello World"),
expectNil: false,
},
{
name: "Valid UTF-16BE input with BOM (FE FF)",
input: []byte{254, 255, 0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100},
expected: []byte("Hello World"),
expectNil: false,
},
{
name: "Invalid UTF-16 input (it's UTF-8)",
input: []byte("Hello World!"),
expected: nil,
expectNil: true,
},
{
name: "Invalid UTF-16 input (odd length)",
input: []byte{72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 0},
expected: []byte("Hello Worl"),
expectNil: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
chunk := &sources.Chunk{Data: tc.input}
decoder := &UTF16{}
decodedChunk := decoder.FromChunk(chunk)
if tc.expectNil {
if decodedChunk != nil {
t.Errorf("Expected nil, got chunk with data: %v", decodedChunk.Data)
}
return
}
if decodedChunk == nil {
t.Errorf("Expected chunk with data, got nil")
return
}
if !bytes.Equal(decodedChunk.Data, tc.expected) {
t.Errorf("Expected decoded data: %s, got: %s", tc.expected, decodedChunk.Data)
}
})
}
}
func TestDLL(t *testing.T) {
data, err := os.ReadFile("utf16_test.dll")
if err != nil {
t.Errorf("Failed to read test data: %v", err)
return
}
chunk := &sources.Chunk{Data: data}
decoder := &UTF16{}
decodedChunk := decoder.FromChunk(chunk)
if decodedChunk == nil {
t.Errorf("Expected chunk with data, got nil")
return
}
if !bytes.Contains(decodedChunk.Data, []byte("aws_secret_access_key")) {
t.Errorf("Expected chunk to have aws_secret_access_key")
return
}
}
func BenchmarkUtf16ToUtf8(b *testing.B) {
// Example UTF-16LE encoded data
data := []byte{72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100, 0}
for b.Loop() {
_, _ = utf16ToUTF8(data)
}
}
================================================
FILE: pkg/decoders/utf8.go
================================================
package decoders
import (
"unicode/utf8"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
type UTF8 struct{}
func (d *UTF8) Type() detectorspb.DecoderType {
return detectorspb.DecoderType_PLAIN
}
func (d *UTF8) FromChunk(chunk *sources.Chunk) *DecodableChunk {
if chunk == nil || len(chunk.Data) == 0 {
return nil
}
decodableChunk := &DecodableChunk{Chunk: chunk, DecoderType: d.Type()}
if !utf8.Valid(chunk.Data) {
chunk.Data = extractSubstrings(chunk.Data)
return decodableChunk
}
return decodableChunk
}
// utf8ReplacementBytes holds the UTF-8 encoded form of the Unicode replacement character (U+FFFD).
// This is pre-computed since it's used frequently when replacing invalid UTF-8 sequences
// and control characters.
var utf8ReplacementBytes = []byte(string(utf8.RuneError))
// extractSubstrings sanitizes byte sequences to ensure consistent handling of malformed input
// while maintaining readable content. It handles ASCII and UTF-8 data as follows:
//
// For ASCII range (0-127): preserves printable characters (32-126) while replacing
// control characters with the UTF-8 replacement character.
// https://cs.opensource.google/go/go/+/refs/tags/go1.23.3:src/unicode/utf8/utf8.go;l=16
//
// For multi-byte sequences: preserves valid UTF-8 as-is, while invalid sequences
// are replaced with a single UTF-8 replacement character.
func extractSubstrings(b []byte) []byte {
dataLen := len(b)
buf := make([]byte, 0, dataLen)
for idx := 0; idx < dataLen; {
// If it's ASCII, handle separately.
// This is faster than decoding for common cases.
if b[idx] < utf8.RuneSelf {
if isPrintableByte(b[idx]) {
buf = append(buf, b[idx])
} else {
buf = append(buf, utf8ReplacementBytes...)
}
idx++
continue
}
r, size := utf8.DecodeRune(b[idx:])
if r == utf8.RuneError {
// Collapse any malformed sequence into a single replacement character
// rather than replacing each byte individually.
buf = append(buf, utf8ReplacementBytes...)
idx++
} else {
// Keep valid multi-byte UTF-8 sequences intact to preserve unicode characters.
buf = append(buf, b[idx:idx+size]...)
idx += size
}
}
return buf
}
// isPrintableByte reports whether a byte represents a printable ASCII character
// using a fast byte-range check. This avoids the overhead of utf8.DecodeRune
// for the common case of ASCII characters (0-127), since we know any byte < 128
// represents a complete ASCII character and doesn't need UTF-8 decoding.
// This includes letters, digits, punctuation, and symbols, but excludes control characters.
// The upper bound is 127 (not 128) because 127 is the DEL control character.
//
// https://www.rapidtables.com/code/text/ascii-table.html
func isPrintableByte(c byte) bool { return c > 31 && c < 127 }
================================================
FILE: pkg/decoders/utf8_test.go
================================================
package decoders
import (
"strings"
"testing"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
func TestUTF8_FromChunk_ValidUTF8(t *testing.T) {
type args struct {
chunk *sources.Chunk
}
tests := []struct {
name string
d *UTF8
args args
want *sources.Chunk
wantErr bool
}{
{
name: "successful UTF8 decode",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte("plain 'ol chunk that should decode successfully")},
},
want: &sources.Chunk{Data: []byte("plain 'ol chunk that should decode successfully")},
wantErr: false,
},
{
name: "empty chunk",
d: &UTF8{},
args: args{
chunk: nil,
},
want: nil,
wantErr: false,
},
{
name: "valid UTF8 with control characters",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte("FIRST_KEY_123456\x00SECOND_KEY_789012")},
},
want: &sources.Chunk{Data: []byte("FIRST_KEY_123456\x00SECOND_KEY_789012")},
wantErr: false,
},
{
name: "valid UTF8 with all ASCII control characters",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte{
'S', 'T', 'A', 'R', 'T',
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14,
0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
'E', 'N', 'D',
}},
},
want: &sources.Chunk{Data: []byte("START\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1FEND")},
wantErr: false,
},
{
name: "aws key in binary data - valid utf8",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte("AWS_ACCESS_KEY_ID\x00\x00\x00AKIAEXAMPLEKEY123\x00")},
},
want: &sources.Chunk{Data: []byte("AWS_ACCESS_KEY_ID\x00\x00\x00AKIAEXAMPLEKEY123\x00")},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &UTF8{}
got := d.FromChunk(tt.args.chunk)
if got != nil && tt.want != nil {
if diff := pretty.Compare(string(got.Data), string(tt.want.Data)); diff != "" {
t.Errorf("%s: UTF8.FromChunk() diff: (-got +want)\n%s", tt.name, diff)
}
} else {
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("%s: UTF8.FromChunk() diff: (-got +want)\n%s", tt.name, diff)
}
}
})
}
}
func TestUTF8_FromChunk_InvalidUTF8(t *testing.T) {
type args struct {
chunk *sources.Chunk
}
tests := []struct {
name string
d *UTF8
args args
want *sources.Chunk
wantErr bool
}{
{
name: "basic invalid utf8",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte("\xF0\x28\x8C\x28")},
},
want: &sources.Chunk{Data: []byte("�(�(")},
wantErr: false,
},
{
name: "invalid utf8 between words",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte("START\xF0\x28\x8C\x28MIDDLE\xC0\x80END")},
},
want: &sources.Chunk{Data: []byte("START�(�(MIDDLE��END")},
wantErr: false,
},
{
name: "binary data with embedded text",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte{
0xF0, 'S', 'E', 'C', 'R', 'E', 'T', // Invalid UTF-8 before text
0xC0, 0x80, // Invalid UTF-8 sequence
'V', 'A', 'L', 'U', 'E',
0xFF, 0x8C, // More invalid UTF-8
}},
},
want: &sources.Chunk{Data: []byte("�SECRET��VALUE��")},
wantErr: false,
},
{
name: "binary protocol with length fields",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte{
0x02, // frame type
0x00, 0x00, 0x00, 0x0A, // length field
'P', 'A', 'S', 'S', 'W', 'O', 'R', 'D', '1', '2',
0xFE, 0xFF, // checksum
}},
},
want: &sources.Chunk{Data: []byte("�����PASSWORD12��")},
wantErr: false,
},
{
name: "truncated utf8 sequence",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte("PREFIX\xF0\x28SUFFIX")},
},
want: &sources.Chunk{Data: []byte("PREFIX�(SUFFIX")},
wantErr: false,
},
{
name: "multiple invalid sequences",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte{
0xF0, 'A', // Invalid + ASCII
0xC0, 0x80, // Invalid sequence
'B',
0xFF, // Single invalid byte
'C',
0xF0, 0x28, 0x8C, 0x28, // Invalid sequence
'D',
}},
},
want: &sources.Chunk{Data: []byte("�A��B�C�(�(D")},
wantErr: false,
},
{
name: "invalid utf8 header with embedded secret",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte{
0xF0, 0x28, 0x8C, // Invalid UTF-8 sequence
'S', 'E', 'C', 'R', 'E', 'T', '=',
0xC0, 0x80, // Another invalid UTF-8 sequence
'A', 'K', 'I', 'A', '1', '2', '3', '4', '5', '6',
0xF8, 0x88, // More invalid UTF-8
}},
},
want: &sources.Chunk{Data: []byte("�(�SECRET=��AKIA123456��")},
wantErr: false,
},
{
name: "key value pairs with length prefixes",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte{
0x00, 0x01, // header
'A', 'P', 'I', '_', 'K', 'E', 'Y', '=',
0x00, 0x00, 0x00, 0x05, // length
'A', 'K', 'I', 'A', '5',
0xFF, // separator
'S', 'E', 'C', 'R', 'E', 'T', '=',
0x00, 0x00, 0x00, 0x06,
'S', 'E', 'C', 'R', 'E', 'T',
}},
},
want: &sources.Chunk{Data: []byte("��API_KEY=����AKIA5�SECRET=����SECRET")},
wantErr: false,
},
{
name: "mixed binary and invalid utf8",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte{
0x00, 0x01, // valid binary
0xF0, 0x28, // invalid UTF-8
'K', 'E', 'Y', '=',
0xC0, 0x80, // more invalid UTF-8
'V', 'A', 'L', 'U', 'E',
}},
},
want: &sources.Chunk{Data: []byte("���(KEY=��VALUE")},
wantErr: false,
},
{
name: "very large utf8 sequence",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte(strings.Repeat("世界", 1000))},
},
want: &sources.Chunk{Data: []byte(strings.Repeat("世界", 1000))},
wantErr: false,
},
{
name: "single byte chunk",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte{0x41}}, // Single 'A'
},
want: &sources.Chunk{Data: []byte("A")},
wantErr: false,
},
{
name: "chunk with zero bytes between valid utf8",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte("hello\x00world\x00!")},
},
want: &sources.Chunk{Data: []byte("hello\x00world\x00!")},
wantErr: false,
},
{
name: "multi-byte unicode characters",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte("🌍🌎🌏")},
},
want: &sources.Chunk{Data: []byte("🌍🌎🌏")},
wantErr: false,
},
{
name: "mixed ascii and multi-byte unicode with invalid sequences",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte("Hello 世界\xF0\x28\x8C\x28Testing🌍")},
},
want: &sources.Chunk{Data: []byte("Hello 世界�(�(Testing🌍")},
wantErr: false,
},
{
name: "chunk ending with partial utf8 sequence",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte("Hello\xE2\x80")}, // Incomplete UTF-8 sequence
},
want: &sources.Chunk{Data: []byte("Hello��")},
wantErr: false,
},
{
name: "chunk with all printable ascii chars",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~")},
},
want: &sources.Chunk{Data: []byte(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~")},
wantErr: false,
},
{
name: "alternating valid and invalid utf8",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte("A\xF0B\xF0C\xF0D")},
},
want: &sources.Chunk{Data: []byte("A�B�C�D")},
wantErr: false,
},
{
name: "overlong utf8 encoding",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte{0xF0, 0x82, 0x82, 0xAC}}, // Overlong encoding of €
},
want: &sources.Chunk{Data: []byte("����")},
wantErr: false,
},
{
name: "utf8 boundary conditions",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte{
0xFF, // Invalid single byte -> �
0xC2, 0x80, // Minimum valid 2-byte UTF-8 sequence (U+0080) -> \u0080
0xDF, 0xBF, // Maximum valid 2-byte UTF-8 sequence (U+07FF) -> ߿
0xE0, 0x80, 0x80, // Invalid 3-byte (overlong encoding) -> �
0xEF, 0xBF, 0xBF, // Valid 3-byte sequence for U+FFFF -> \uffff
0xF0, 0x28, 0x8C, 0x28, // Invalid UTF-8 mixed with ASCII -> �(�(
0xF4, 0x8F, 0xBF, 0xBF, // Valid 4-byte sequence for U+10FFFF -> \U0010ffff
}},
},
want: &sources.Chunk{Data: []byte("�\u0080߿���\uffff�(�(\U0010ffff")},
wantErr: false,
},
{
name: "chunk with byte order mark (BOM)",
d: &UTF8{},
args: args{
chunk: &sources.Chunk{Data: []byte{0xEF, 0xBB, 0xBF, 'h', 'e', 'l', 'l', 'o'}},
},
want: &sources.Chunk{Data: []byte("\uFEFFhello")},
wantErr: false,
},
{
name: "chunk with surrogate pairs",
d: &UTF8{},
args: args{
// Invalid UTF-8 encoding of surrogate pairs
chunk: &sources.Chunk{Data: []byte{0xED, 0xA0, 0x80, 0xED, 0xB0, 0x80}},
},
want: &sources.Chunk{Data: []byte("������")},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &UTF8{}
got := d.FromChunk(tt.args.chunk)
if got != nil && tt.want != nil {
if diff := pretty.Compare(string(got.Data), string(tt.want.Data)); diff != "" {
t.Errorf("%s: UTF8.FromChunk() diff: (-got +want)\n%s", tt.name, diff)
}
} else {
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("%s: UTF8.FromChunk() diff: (-got +want)\n%s", tt.name, diff)
}
}
})
}
}
var testBytes = []byte(`some words with random spaces and
newlines with
arbitrary length
of
hey
the lines themselves.
and
short
words
that
go
away.`)
func Benchmark_extractSubstrings(b *testing.B) {
for b.Loop() {
extractSubstrings(testBytes)
}
}
================================================
FILE: pkg/detectors/abstract/abstract.go
================================================
package abstract
import (
"context"
"fmt"
regexp "github.com/wasilibs/go-re2"
"net/http"
"strings"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
}
const abstractURL = "https://exchange-rates.abstractapi.com"
var (
// Ensure the Scanner satisfies the interface at compile time.
_ detectors.Detector = (*Scanner)(nil)
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"abstract"}) + `\b([0-9a-z]{32})\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{"abstract"}
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Abstract 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Abstract,
Raw: []byte(resMatch),
}
if verify {
client := s.getClient()
isVerified, verificationErr := verifyAbstract(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyAbstract(ctx context.Context, client *http.Client, resMatch string) (bool, error) {
// https://docs.abstractapi.com/exchange-rates#response-and-error-codes
req, err := http.NewRequestWithContext(ctx, http.MethodGet, abstractURL+fmt.Sprintf("/v1/live/?api_key=%s&base=USD", resMatch), nil)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
return false, err
}
defer res.Body.Close()
// https://docs.abstractapi.com/exchange-rates#response-and-error-codes
switch res.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Abstract
}
func (s Scanner) Description() string {
return "Abstract API provides various services including exchange rates. The API keys can be used to access these services and retrieve data."
}
================================================
FILE: pkg/detectors/abstract/abstract_integration_test.go
================================================
//go:build detectors
// +build detectors
package abstract
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/detectorspb"
)
func TestAbstract_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("ABSTRACT")
inactiveSecret := testSecrets.MustGetField("ABSTRACT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a abstract secret %s within but verified", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Abstract,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, real secrets, verification error due to timeout",
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a abstract secret %s within but verified", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Abstract,
Verified: false,
}
r.SetVerificationError(context.DeadlineExceeded)
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, real secrets, verification error due to unexpected api surface",
s: Scanner{client: common.ConstantResponseHttpClient(500, "{}")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a abstract secret %s within", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Abstract,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 500"))
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a abstract secret %s within but not valid", inactiveSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Abstract,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
time.Sleep(900 * time.Millisecond) // avoid rate limiting
got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Abstract.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")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Abstract.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/abstract/abstract_test.go
================================================
package abstract
import (
"context"
"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"
)
func TestAbstract_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to abstract API
[DEBUG] Using API_KEY=oxpf4a93fjovt0v1z6lltcbcizlrml98
[INFO] Response received: 200 OK
`,
want: []string{"oxpf4a93fjovt0v1z6lltcbcizlrml98"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{abstract}{abstract AQAAABAAA 5422358j60yxo9nc0dbpxby602tsxd6j}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"5422358j60yxo9nc0dbpxby602tsxd6j"},
},
{
name: "valid pattern - two keys",
input: `
[INFO] Sending request to abstract API
[DEBUG] Using API_KEY=oxpf4a93fjovt0v1z6lltcbcizlrml98
[Error] Response received: 401 UnAuthorized
[INFO] Sending request to abstract API
[DEBUG] Using API_KEY=muytrs09876iugt67s7a7sa0akhsxz82
[INFO] Response received: 200 OK
`,
want: []string{"oxpf4a93fjovt0v1z6lltcbcizlrml98", "muytrs09876iugt67s7a7sa0akhsxz82"},
},
{
name: "valid pattern - out of prefix range",
input: `
[INFO] Sending request to abstract API
[INFO] Processing request
[Info] Response received: 200 OK
[DEBUG] Used API_KEY=oxpf4a93fjovt0v1z6lltcbcizlrml98
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to abstract API
[DEBUG] Using API_KEY=zxcvbr12345iugt67s7a7sa0akhsXz820
[ERROR] Response received: 400 BadRequest
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/abuseipdb/abuseipdb.go
================================================
package abuseipdb
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
const abuseipdbURL = "https://api.abuseipdb.com"
var (
// Ensure the Scanner satisfies the interface at compile time.
_ detectors.Detector = (*Scanner)(nil)
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"abuseipdb"}) + `\b([a-z0-9]{80})\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{"abuseipdb"}
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify AbuseIPDB 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AbuseIPDB,
Raw: []byte(resMatch),
}
if verify {
client := s.getClient()
isVerified, verificationErr := verifyAbuseIPDB(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyAbuseIPDB(ctx context.Context, client *http.Client, resMatch string) (bool, error) {
// https://docs.abuseipdb.com/#check-endpoint
req, err := http.NewRequestWithContext(ctx, http.MethodGet, abuseipdbURL+"/api/v2/check?ipAddress=8.8.8.8", nil)
if err != nil {
return false, err
}
req.Header.Add("Key", resMatch)
res, err := client.Do(req)
if err != nil {
return false, err
}
defer res.Body.Close()
switch res.StatusCode {
case http.StatusOK:
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
validResponse := bytes.Contains(bodyBytes, []byte("ipAddress"))
if validResponse {
return true, nil
} else {
return false, nil
}
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AbuseIPDB
}
func (s Scanner) Description() string {
return "AbuseIPDB is a project dedicated to helping combat the spread of hackers, spammers, and abusive activity on the internet. AbuseIPDB API keys can be used to report and check IP addresses for abusive activities."
}
================================================
FILE: pkg/detectors/abuseipdb/abuseipdb_integration_test.go
================================================
//go:build detectors
// +build detectors
package abuseipdb
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/detectorspb"
)
func TestAbuseIPDB_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ABUSEIPDB")
inactiveSecret := testSecrets.MustGetField("ABUSEIPDB_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a abuseipdb secret %s within but verified", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AbuseIPDB,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, real secrets, verification error due to timeout",
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a abuseipdb secret %s within", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_AbuseIPDB,
Verified: false,
}
r.SetVerificationError(context.DeadlineExceeded)
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, real secrets, verification error due to unexpected api surface",
s: Scanner{client: common.ConstantResponseHttpClient(500, "{}")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a abuseipdb secret %s within", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_AbuseIPDB,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 500"))
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a abuseipdb secret %s within", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AbuseIPDB,
Verified: false,
},
},
wantErr: 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,
},
}
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("AbuseIPDB.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")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AbuseIPDB.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/abuseipdb/abuseipdb_test.go
================================================
package abuseipdb
import (
"context"
"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"
)
func TestAbuseipdb_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to abuseipdb API
[DEBUG] Using API_KEY=o8oqti3tghu2xic76ii4t7jb9bxuzd4200j1yrkdjl6s8834hx4dgz1wwo90diqraakjd13sljcjkfnf
[INFO] Response received: 200 OK
`,
want: []string{"o8oqti3tghu2xic76ii4t7jb9bxuzd4200j1yrkdjl6s8834hx4dgz1wwo90diqraakjd13sljcjkfnf"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{abuseipdb}{abuseipdb AQAAABAAA zgtj0q3v38u4pthc6nmy02n60bj244u5o9j47ln1jlue5mxzaasfi29x4dzcbxroawvkm26thtr61066}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"zgtj0q3v38u4pthc6nmy02n60bj244u5o9j47ln1jlue5mxzaasfi29x4dzcbxroawvkm26thtr61066"},
},
{
name: "valid pattern - out of prefix range",
input: `
[INFO] Sending request to abuseipdb API
[INFO] Processing request
[Info] Response received: 200 OK
[DEBUG] Used API_KEY=o8oqti3tghu2xic76ii4t7jb9bxuzd4200j1yrkdjl6s8834hx4dgz1wwo90diqraakjd13sljcjkfnf
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to abuseipdb API
[DEBUG] Using API_KEY=7e4abcdef456Ghijkl789mnopqr012stuvwx3455123abcdef456ghijkl789mnopqr012stuvwX
[ERROR] Response received: 400 BadRequest
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/abyssale/abyssale.go
================================================
package abyssale
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
const abyssaleURL = "https://api.abyssale.com"
var (
// Ensure the Scanner satisfies the interface at compile time.
_ detectors.Detector = (*Scanner)(nil)
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"abyssale"}) + `\b([a-z0-9A-Z]{40})\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{"abyssale"}
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Abyssale 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Abyssale,
Raw: []byte(resMatch),
}
if verify {
client := s.getClient()
isVerified, verificationErr := verifyAbyssale(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyAbyssale(ctx context.Context, client *http.Client, resMatch string) (bool, error) {
// https://developers.abyssale.com/rest-api/authentication
req, err := http.NewRequestWithContext(ctx, http.MethodGet, abyssaleURL+"/ready", nil)
if err != nil {
return false, err
}
req.Header.Add("x-api-key", resMatch)
res, err := client.Do(req)
if err != nil {
return false, err
}
defer res.Body.Close()
switch res.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Abyssale
}
func (s Scanner) Description() string {
return "Abyssale is a service offering various API functionalities for marketing automation and services such as images and ad campaigns. Abyssale API keys can be used to access and interact with this data."
}
================================================
FILE: pkg/detectors/abyssale/abyssale_integration_test.go
================================================
//go:build detectors
// +build detectors
package abyssale
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
)
func TestAbyssale_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("ABYSSALE_TOKEN")
inactiveSecret := testSecrets.MustGetField("ABYSSALE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a abyssale secret %s within but verified", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Abyssale,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, real secrets, verification error due to timeout",
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a abyssale secret %s within but verified", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Abyssale,
Verified: false,
}
r.SetVerificationError(context.DeadlineExceeded)
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, real secrets, verification error due to unexpected api surface",
s: Scanner{client: common.ConstantResponseHttpClient(500, "{}")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a abyssale secret %s within but verified", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Abyssale,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 500"))
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a abyssale secret %s within but verified", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Abyssale,
Verified: false,
},
},
wantErr: 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,
},
}
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("Abyssale.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")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Abyssale.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/abyssale/abyssale_test.go
================================================
package abyssale
import (
"context"
"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"
)
func TestAbyssale_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to abyssale API
[DEBUG] Using API_KEY=rWE8I0axy6Fvw40RE8tsNS3L7zBU5vAhEnW4hq9G
[INFO] Response received: 200 OK
`,
want: []string{"rWE8I0axy6Fvw40RE8tsNS3L7zBU5vAhEnW4hq9G"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{abyssale}{abyssale AQAAABAAA xTiPNSDg6JjzG8fWoLb8JlE8SBcMKkCx2fZLZD91}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"xTiPNSDg6JjzG8fWoLb8JlE8SBcMKkCx2fZLZD91"},
},
{
name: "valid pattern - out of prefix range",
input: `
[INFO] Sending request to abyssale API
[INFO] Processing request
[Info] Response received: 200 OK
[DEBUG] Used API_KEY=rWE8I0axy6Fvw40RE8tsNS3L7zBU5vAhEnW4hq9G
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to abyssale API
[DEBUG] Using API_KEY=rWE8_0axy6Fvw40RE8tsNS3L7zBU5vAhEnW4hq9G
[ERROR] Response received: 400 BadRequest
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/account_filter.go
================================================
package detectors
// AccountFilter implements account-based filtering functionality that detectors can embed
// to gain allow and deny list capabilities for account IDs.
type AccountFilter struct {
allowedAccounts map[string]struct{}
deniedAccounts map[string]struct{}
}
// SetAllowedAccounts configures the allowed account IDs.
// If set, only accounts in this list will be verified.
func (a *AccountFilter) SetAllowedAccounts(accountIDs []string) {
if len(accountIDs) == 0 {
a.allowedAccounts = nil
return
}
accounts := make(map[string]struct{}, len(accountIDs))
for _, accountID := range accountIDs {
accounts[accountID] = struct{}{}
}
a.allowedAccounts = accounts
}
// SetDeniedAccounts configures the denied account IDs.
// Accounts in this list will never be verified.
func (a *AccountFilter) SetDeniedAccounts(accountIDs []string) {
if len(accountIDs) == 0 {
a.deniedAccounts = nil
return
}
accounts := make(map[string]struct{}, len(accountIDs))
for _, accountID := range accountIDs {
accounts[accountID] = struct{}{}
}
a.deniedAccounts = accounts
}
// ShouldSkipAccount checks if an account ID should be skipped for verification
// based on allow and deny lists.
//
// Precedence: deny list > allow list (if account is in both, it's denied)
func (a *AccountFilter) ShouldSkipAccount(accountID string) bool {
// Check deny list first - takes precedence
if len(a.deniedAccounts) > 0 {
if _, isDenied := a.deniedAccounts[accountID]; isDenied {
return true
}
}
// Check allow list - if populated, account must be in it
if len(a.allowedAccounts) > 0 {
if _, isAllowed := a.allowedAccounts[accountID]; !isAllowed {
return true
}
}
// Account is allowed for verification
return false
}
// IsInDenyList checks if an account ID is in the deny list
func (a *AccountFilter) IsInDenyList(accountID string) bool {
if len(a.deniedAccounts) == 0 {
return false
}
_, isDenied := a.deniedAccounts[accountID]
return isDenied
}
// IsInAllowList checks if an account ID is in the allow list
func (a *AccountFilter) IsInAllowList(accountID string) bool {
if len(a.allowedAccounts) == 0 {
return false
}
_, isAllowed := a.allowedAccounts[accountID]
return isAllowed
}
================================================
FILE: pkg/detectors/account_filter_test.go
================================================
package detectors
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEmbeddedAccountFilter(t *testing.T) {
type Scanner struct{ AccountFilter }
t.Run("no filtering configured - should not skip", func(t *testing.T) {
var s Scanner // Fresh instance for this test
shouldSkip := s.ShouldSkipAccount("test-account")
assert.False(t, shouldSkip)
assert.False(t, s.IsInDenyList("test-account"))
assert.False(t, s.IsInAllowList("test-account"))
})
t.Run("allowed accounts only", func(t *testing.T) {
var s Scanner // Fresh instance for this test
s.SetAllowedAccounts([]string{"allowed-account-1", "allowed-account-2"})
// Account in allow list - should not skip
shouldSkip := s.ShouldSkipAccount("allowed-account-1")
assert.False(t, shouldSkip)
assert.True(t, s.IsInAllowList("allowed-account-1"))
// Account not in allow list - should skip
shouldSkip = s.ShouldSkipAccount("other-account")
assert.True(t, shouldSkip)
assert.False(t, s.IsInAllowList("other-account"))
})
t.Run("denied accounts only", func(t *testing.T) {
var s Scanner // Fresh instance for this test
s.SetDeniedAccounts([]string{"denied-account-1", "denied-account-2"})
// Account in deny list - should skip
shouldSkip := s.ShouldSkipAccount("denied-account-1")
assert.True(t, shouldSkip)
assert.True(t, s.IsInDenyList("denied-account-1"))
// Account not in deny list - should not skip (no allow list restrictions)
shouldSkip = s.ShouldSkipAccount("other-account")
assert.False(t, shouldSkip)
assert.False(t, s.IsInDenyList("other-account"))
})
t.Run("deny list takes precedence over allow list", func(t *testing.T) {
var s Scanner // Fresh instance for this test
s.SetAllowedAccounts([]string{"conflicted-account", "allowed-only-account"})
s.SetDeniedAccounts([]string{"conflicted-account"}) // Same account in both lists
// Account is in both allow and deny lists - deny takes precedence
shouldSkip := s.ShouldSkipAccount("conflicted-account")
assert.True(t, shouldSkip)
assert.True(t, s.IsInDenyList("conflicted-account"))
assert.True(t, s.IsInAllowList("conflicted-account"))
// Account only in allow list - should not skip
shouldSkip = s.ShouldSkipAccount("allowed-only-account")
assert.False(t, shouldSkip)
assert.False(t, s.IsInDenyList("allowed-only-account"))
assert.True(t, s.IsInAllowList("allowed-only-account"))
})
t.Run("allow list with denied account not in allow list", func(t *testing.T) {
var s Scanner // Fresh instance for this test
s.SetAllowedAccounts([]string{"trusted-account"}) // Allow one account
s.SetDeniedAccounts([]string{"blocked-account"}) // Deny different account
// Account in deny list (not in allow list) - should skip due to deny list
shouldSkip := s.ShouldSkipAccount("blocked-account")
assert.True(t, shouldSkip)
assert.True(t, s.IsInDenyList("blocked-account"))
assert.False(t, s.IsInAllowList("blocked-account"))
// Account in allow list (not in deny list) - should not skip
shouldSkip = s.ShouldSkipAccount("trusted-account")
assert.False(t, shouldSkip)
assert.False(t, s.IsInDenyList("trusted-account"))
assert.True(t, s.IsInAllowList("trusted-account"))
// Account in neither list - should skip due to allow list restriction
shouldSkip = s.ShouldSkipAccount("unknown-account")
assert.True(t, shouldSkip)
assert.False(t, s.IsInDenyList("unknown-account"))
assert.False(t, s.IsInAllowList("unknown-account"))
})
t.Run("clearing lists", func(t *testing.T) {
var s Scanner // Fresh instance for this test
s.SetAllowedAccounts([]string{"initial-allowed"})
s.SetDeniedAccounts([]string{"initial-denied"})
// Verify initial state
assert.True(t, s.ShouldSkipAccount("random-account")) // Not in allow list
assert.True(t, s.ShouldSkipAccount("initial-denied")) // In deny list
// Clear allowed accounts with nil
s.SetAllowedAccounts(nil)
assert.False(t, s.ShouldSkipAccount("random-account")) // No allow list restriction
assert.True(t, s.ShouldSkipAccount("initial-denied")) // Still in deny list
// Clear denied accounts with empty slice
s.SetDeniedAccounts([]string{})
assert.False(t, s.ShouldSkipAccount("initial-denied")) // No longer denied
assert.False(t, s.ShouldSkipAccount("initial-allowed")) // No restrictions
})
}
================================================
FILE: pkg/detectors/accuweather/v1/accuweather.go
================================================
package accuweather
import (
"context"
"fmt"
"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/detectorspb"
)
type Scanner struct {
Client *http.Client
}
const accuweatherURL = "https://dataservice.accuweather.com"
const requiredShannonEntropy = 4
var (
// Ensure the Scanner satisfies the interface at compile time.
_ detectors.Detector = (*Scanner)(nil)
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"accuweather"}) + `([a-z0-9A-Z\%]{35})\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{"accuweather"}
}
func (s Scanner) Version() int { return 1 }
func (s Scanner) getClient() *http.Client {
if s.Client != nil {
return s.Client
}
return defaultClient
}
// FromData will find and optionally verify Accuweather secrets in a given set of bytes.
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
matches := keyPat.FindAllStringSubmatch(string(data), -1)
return s.ProcessMatches(ctx, matches, verify)
}
func (s Scanner) ProcessMatches(ctx context.Context, matches [][]string, verify bool) (results []detectors.Result, err error) {
uniqueMatches := getUniqueMatches(matches)
for key := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Accuweather,
Raw: []byte(key),
}
if verify {
client := s.getClient()
isVerified, verificationErr := verifyAccuweather(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, key)
}
results = append(results, s1)
}
return
}
func getUniqueMatches(allMatches [][]string) map[string]struct{} {
uniqueMatches := map[string]struct{}{}
for _, match := range allMatches {
k := match[1]
if detectors.StringShannonEntropy(k) < requiredShannonEntropy {
continue
}
uniqueMatches[k] = struct{}{}
}
return uniqueMatches
}
func verifyAccuweather(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, accuweatherURL+"/locations/v1/cities/autocomplete?apikey="+key+"&q=----&language=en-us", nil)
if err != nil {
return false, err
}
res, err := client.Do(req)
if err != nil {
return false, err
}
defer res.Body.Close()
// https://developer.accuweather.com/accuweather-locations-api/apis/get/locations/v1/cities/autocomplete
switch res.StatusCode {
case http.StatusOK, http.StatusForbidden:
// 403 indicates lack of permission, but valid token
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Accuweather
}
func (s Scanner) Description() string {
return "AccuWeather is a weather forecasting service. AccuWeather API keys can be used to access weather data and forecasts."
}
================================================
FILE: pkg/detectors/accuweather/v1/accuweather_integration_test.go
================================================
//go:build detectors
// +build detectors
package accuweather
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/detectorspb"
)
func TestAccuweather_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ACCUWEATHER")
inactiveSecret := testSecrets.MustGetField("ACCUWEATHER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a accuweather secret %s within but verified", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Accuweather,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, real secrets, verification error due to timeout",
s: Scanner{Client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a accuweather secret %s within", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Accuweather,
Verified: false,
}
r.SetVerificationError(context.DeadlineExceeded)
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, real secrets, verification error due to unexpected api surface",
s: Scanner{Client: common.ConstantResponseHttpClient(500, "{}")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a accuweather secret %s within", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Accuweather,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 500"))
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a accuweather secret %s within but verified", inactiveSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Accuweather,
Verified: false,
},
},
wantErr: 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,
},
}
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("Accuweather.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())
}
got[i].Raw = nil
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Accuweather.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/accuweather/v1/accuweather_test.go
================================================
package accuweather
import (
"context"
"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"
)
func TestAccuWeather_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to accuweather API
[DEBUG] Using API_KEY=WAgP6m4gYc1qe%HnjWAAF5HBKL%i6kwrsbD
[INFO] Response received: 200 OK
`,
want: []string{"WAgP6m4gYc1qe%HnjWAAF5HBKL%i6kwrsbD"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{accuweather}{accuweather AQAAABAAA ErOAU9rTSuX6IfHFGsJbpK3bCC1jIEX%gtj}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"ErOAU9rTSuX6IfHFGsJbpK3bCC1jIEX%gtj"},
},
{
name: "valid pattern - out of prefix range",
input: `
[INFO] Sending request to accuweather API
[INFO] Processing request
[Info] Response received: 200 OK
[DEBUG] Used API_KEY=WAgP6m4gYc1qe%HnjWAAF5HBKL%i6kwrsbD
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to accuweather API
[DEBUG] Using API_KEY=WAgP6m4gYc1qe$HnjWAAF5HBKL%i6kwrsbD
[Error] Response received: 400 BadRequest
`,
want: nil,
},
{
name: "valid pattern - Shannon entropy below threshold",
input: `
[INFO] Sending request to accuweather API
[DEBUG] Using API_KEY=WAAP6A4gYA1qA%HAaWAAFAHBAL%a6kwwwbD
[ERROR] Response received: 400 BadRequest
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/accuweather/v2/accuweather.go
================================================
package accuweather
import (
"context"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
v1 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/accuweather/v1"
)
type Scanner struct {
v1.Scanner
}
func (s Scanner) Version() int { return 2 }
var (
// Ensure the Scanner satisfies the interface at compile time.
_ detectors.Detector = (*Scanner)(nil)
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"accuweather"}) + `\b([a-zA-Z0-9]{32})\b`)
)
// FromData will find and optionally verify Accuweather secrets in a given set of bytes.
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
matches := keyPat.FindAllStringSubmatch(string(data), -1)
return s.ProcessMatches(ctx, matches, verify)
}
================================================
FILE: pkg/detectors/accuweather/v2/accuweather_integration_test.go
================================================
//go:build detectors
// +build detectors
package accuweather
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"
v1 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/accuweather/v1"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAccuweather_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("ACCUWEATHER")
inactiveSecret := testSecrets.MustGetField("ACCUWEATHER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a accuweather secret %s within but verified", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Accuweather,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, real secrets, verification error due to timeout",
s: Scanner{Scanner: v1.Scanner{Client: common.SaneHttpClientTimeOut(1 * time.Microsecond)}},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a accuweather secret %s within", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Accuweather,
Verified: false,
}
r.SetVerificationError(context.DeadlineExceeded)
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, real secrets, verification error due to unexpected api surface",
s: Scanner{Scanner: v1.Scanner{Client: common.ConstantResponseHttpClient(500, "{}")}},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a accuweather secret %s within", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Accuweather,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 500"))
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a accuweather secret %s within but verified", inactiveSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Accuweather,
Verified: false,
},
},
wantErr: 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,
},
}
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("Accuweather.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())
}
got[i].Raw = nil
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Accuweather.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/accuweather/v2/accuweather_test.go
================================================
package accuweather
import (
"context"
"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"
)
func TestAccuWeather_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to accuweather API
[DEBUG] Using API_KEY=Qh6DP6Zf7vHtmnDDsbS219qcz4d883Y9
[INFO] Response received: 200 OK
`,
want: []string{"Qh6DP6Zf7vHtmnDDsbS219qcz4d883Y9"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{accuweather}{accuweather AQAAABAAA BJDD9bYh8bR586Wcw3F1lvkUYy3RZZbD}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"BJDD9bYh8bR586Wcw3F1lvkUYy3RZZbD"},
},
{
name: "valid pattern - out of prefix range",
input: `
[INFO] Sending request to accuweather API
[INFO] Processing request
[Info] Response received: 200 OK
[DEBUG] Used API_KEY=Qh6DP6Zf7vHtmnDDsbS219qcz4d883Y9
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to accuweather API
[DEBUG] Using API_KEY=Qh6DP6Zf7vHtm@DDsbS219qcz4d883Y9
[ERROR] Response received: 400 BadRequest
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/adafruitio/adafruitio.go
================================================
package adafruitio
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
const adafruitioURL = "https://io.adafruit.com"
var (
// Ensure the Scanner satisfies the interface at compile time.
_ detectors.Detector = (*Scanner)(nil)
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(aio\_[a-zA-Z0-9]{28})\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{"aio_"}
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify AdafruitIO 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AdafruitIO,
Raw: []byte(resMatch),
}
if verify {
client := s.getClient()
isVerified, verificationErr := verifyAdafruitIO(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyAdafruitIO(ctx context.Context, client *http.Client, resMatch string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, adafruitioURL+"/api/v2/ladybugtest/feeds/?x-aio-key="+resMatch, nil)
if err != nil {
return false, err
}
res, err := client.Do(req)
if err != nil {
return false, err
}
defer res.Body.Close()
// https://learn.adafruit.com/adafruit-io/http-status-codes
switch res.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AdafruitIO
}
func (s Scanner) Description() string {
return "Adafruit IO is a cloud service used for IoT applications. Adafruit IO keys can be used to access and control data and devices connected to the platform."
}
================================================
FILE: pkg/detectors/adafruitio/adafruitio_integration_test.go
================================================
//go:build detectors
// +build detectors
package adafruitio
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/detectorspb"
)
func TestAdafruitIO_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ADAFRUITIO")
inactiveSecret := testSecrets.MustGetField("ADAFRUITIO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a adafruitio secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AdafruitIO,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, real secrets, verification error due to timeout",
s: Scanner{client: common.SaneHttpClientTimeOut(10 * time.Microsecond)},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a adafruitio secret %s within", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_AdafruitIO,
Verified: false,
}
r.SetVerificationError(context.DeadlineExceeded)
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, real secrets, verification error due to unexpected api surface",
s: Scanner{client: common.ConstantResponseHttpClient(500, "{}")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a adafruitio secret %s within", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_AdafruitIO,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 500"))
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a adafruitio secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AdafruitIO,
Verified: false,
},
},
wantErr: 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,
},
}
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("AdafruitIO.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")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AdafruitIO.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/adafruitio/adafruitio_test.go
================================================
package adafruitio
import (
"context"
"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"
)
func TestAdafruitio_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using API_KEY=aio_VxEqGaqgMgZej3DceezbBy03eWyW
[INFO] Response received: 200 OK
`,
want: []string{"aio_VxEqGaqgMgZej3DceezbBy03eWyW"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{adafruitio}{AQAAABAAA aio_cQD77DF9SgsYbgWcxJbpLOlR5emX}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"aio_cQD77DF9SgsYbgWcxJbpLOlR5emX"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using API_KEY=aio_VxEqGaqgMgZej3DceezbBy03eWyWa
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/adobeio/adobeio.go
================================================
package adobeio
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"adobe"}) + `\b([a-z0-9]{32})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"adobe"}) + `\b([a-zA-Z0-9.]{12})\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{"adobe"}
}
// FromData will find and optionally verify AdobeIO 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)
var uniqueKeys, uniqueIds = make(map[string]struct{}), make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for _, matches := range idPat.FindAllStringSubmatch(dataStr, -1) {
uniqueIds[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
for id := range uniqueIds {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AdobeIO,
Raw: []byte(key),
RawV2: []byte(key + id),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAdobeIOSecret(ctx, client, key, id)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyAdobeIOSecret(ctx context.Context, client *http.Client, key string, id string) (bool, error) {
url := "https://stock.adobe.io/Rest/Media/1/Search/Files?locale=en_US%2526search_parameters%255Bwords%255D=kittens"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return false, err
}
req.Header.Add("x-api-key", key)
req.Header.Add("x-product", id)
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AdobeIO
}
func (s Scanner) Description() string {
return "AdobeIO provides APIs for integrating with Adobe services. These credentials can be used to access Adobe services and data."
}
================================================
FILE: pkg/detectors/adobeio/adobeio_integration_test.go
================================================
//go:build detectors
// +build detectors
package adobeio
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAdobeIO_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ADOBEIO_TOKEN")
id := testSecrets.MustGetField("ADOBEIO_PRODUCT")
inactiveSecret := testSecrets.MustGetField("ADOBEIO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a adobeio secret %s within adobeio %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AdobeIO,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a adobeio secret %s within adobeio %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AdobeIO,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AdobeIO.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("AdobeIO.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/adobeio/adobeio_test.go
================================================
package adobeio
import (
"context"
"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"
)
func TestAdobeIO_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the adobe API
[DEBUG] Using adobe KEY=zoaw0c0m50m0hz2h1fm21y4tqfyl7ifi
[DEBUG] Using adobe ID=qCRbiIy1NJaW
[INFO] Response received: 200 OK
`,
want: []string{"zoaw0c0m50m0hz2h1fm21y4tqfyl7ifiqCRbiIy1NJaW"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{adobe ftd7hkeafk0q}{adobe AQAAABAAA siybmtkgho9nsgjhng5yhp92wnir2a9t}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"siybmtkgho9nsgjhng5yhp92wnir2a9tftd7hkeafk0q"},
},
{
name: "valid pattern - out of prefix range",
input: `
[INFO] Sending request to the adobe API
[DEBUG] Using KEY=zoaw0c0m50m0hz2h1fm21y4tqfyl7ifi
[DEBUG] Using ID=qCRbiIy1NJaW
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the adobe API
[DEBUG] Using adobe KEY=Rzxc#0987$%bv1234poiu6749gtnrfv54
[DEBUG] Using adobe ID=qCRbiIy1NJaW
[ERROR] Response received: 400 BadRequest
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/adzuna/adzuna.go
================================================
package adzuna
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
const adzunaURL = "https://api.adzuna.com"
var (
// Ensure the Scanner satisfies the interface at compile time.
_ detectors.Detector = (*Scanner)(nil)
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"adzuna"}) + `\b([a-z0-9]{32})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"adzuna"}) + `\b([a-z0-9]{8})\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{"adzuna"}
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Adzuna 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
resIdMatch := strings.TrimSpace(idMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Adzuna,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch),
}
if verify {
client := s.getClient()
isVerified, verificationErr := verifyAdzuna(ctx, client, resMatch, resIdMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyAdzuna(ctx context.Context, client *http.Client, resMatch, resIdMatch string) (bool, error) {
// https://developer.adzuna.com/activedocs#!/adzuna/search
req, err := http.NewRequestWithContext(ctx, http.MethodGet, adzunaURL+fmt.Sprintf("/v1/api/jobs/us/search/1?app_id=%s&app_key=%s", resIdMatch, resMatch), nil)
if err != nil {
return false, err
}
res, err := client.Do(req)
if err != nil {
return false, err
}
defer res.Body.Close()
// https://developer.adzuna.com/overview
switch res.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Adzuna
}
func (s Scanner) Description() string {
return "Adzuna is a job search engine used to find job listings. Adzuna API keys can be used to access job listing data."
}
================================================
FILE: pkg/detectors/adzuna/adzuna_integration_test.go
================================================
//go:build detectors
// +build detectors
package adzuna
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/detectorspb"
)
func TestAdzuna_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ADZUNA")
id := testSecrets.MustGetField("ADZUNA_ID")
inactiveSecret := testSecrets.MustGetField("ADZUNA_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a adzuna secret %s within adzuna %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Adzuna,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, real secrets, verification error due to timeout",
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a adzuna secret %s within adzuna %s", secret, id)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Adzuna,
Verified: false,
}
r.SetVerificationError(context.DeadlineExceeded)
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, real secrets, verification error due to unexpected api surface",
s: Scanner{client: common.ConstantResponseHttpClient(500, "{}")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a adzuna secret %s within adzuna %s", secret, id)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Adzuna,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 500"))
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a adzuna secret %s within adzuna %s but not valid", inactiveSecret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Adzuna,
Verified: false,
},
},
wantErr: 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,
},
}
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("Adzuna.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")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Adzuna.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/adzuna/adzuna_test.go
================================================
package adzuna
import (
"context"
"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"
)
func TestAdzuna_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the adzuna API
[DEBUG] Using adzuna KEY=smcud4y6elxx7u6q58ewwv8rq01hpi3f
[DEBUG] Using adzuna ID=cxu9w2g6
[INFO] Response received: 200 OK
`,
want: []string{"smcud4y6elxx7u6q58ewwv8rq01hpi3fcxu9w2g6"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{adzuna svkit0wx}{adzuna AQAAABAAA atubvgvpd6jjo0ac1wjianofnpgr24ac}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"atubvgvpd6jjo0ac1wjianofnpgr24acsvkit0wx"},
},
{
name: "valid pattern - out of prefix range",
input: `
[INFO] Sending request to the adzuna API
[DEBUG] Using KEY=smcud4y6elxx7u6q58ewwv8rq01hpi3f
[DEBUG] Using ID=cxu9w2g6
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "valid pattern - only key",
input: `
[INFO] Sending request to the adzuna API
[DEBUG] Using KEY=smcud4y6elxx7u6q58ewwv8rq01hpi3f
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "valid pattern - only id",
input: `
[INFO] Sending request to the adzuna API
[DEBUG] Using ID=cxu9w2g6
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the adzuna API
[DEBUG] Using KEY=sxojb6ygb2wsx0o
[DEBUG] Using ID=cxu9w2g6
[ERROR] Response received: 400 BadRequest
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/aeroworkflow/aeroworkflow.go
================================================
package aeroworkflow
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
client *http.Client
}
const aeroworkflowURL = "https://api.aeroworkflow.com"
var (
// Ensure the Scanner satisfies the interface at compile time.
_ detectors.Detector = (*Scanner)(nil)
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"aeroworkflow"}) + `\b([a-zA-Z0-9^!?#:*;]{20})`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"aeroworkflow"}) + `\b([0-9]{1,})\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{"aeroworkflow"}
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Aeroworkflow 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idmatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idmatch := range idmatches {
resIdMatch := strings.TrimSpace(idmatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Aeroworkflow,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch),
}
if verify {
client := s.getClient()
isVerified, verificationErr := verifyAeroworkflow(ctx, client, resMatch, resIdMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyAeroworkflow(ctx context.Context, client *http.Client, resMatch, resIdMatch string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, aeroworkflowURL+"/api/"+resIdMatch+"/me", nil)
if err != nil {
return false, err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("apikey", resMatch)
res, err := client.Do(req)
if err != nil {
return false, err
}
defer res.Body.Close()
// https://api.aeroworkflow.com/swagger/index.html
switch res.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized, http.StatusForbidden:
// 401 for invalid API key
// 403 for invalid Account ID
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Aeroworkflow
}
func (s Scanner) Description() string {
return "Aeroworkflow is a service for managing workflows. Aeroworkflow API keys and Account IDs can be used to access and manage workflows."
}
================================================
FILE: pkg/detectors/aeroworkflow/aeroworkflow_integration_test.go
================================================
//go:build detectors
// +build detectors
package aeroworkflow
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/detectorspb"
)
func TestAeroworkflow_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("AEROWORKFLOW_SECRET")
id := testSecrets.MustGetField("AEROWORKFLOW_ID")
inactiveSecret := testSecrets.MustGetField("AEROWORKFLOW_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aeroworkflow secret %s within aeroworkflow %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Aeroworkflow,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, real secrets, verification error due to timeout",
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aeroworkflow secret %s within aeroworkflow %s", secret, id)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Aeroworkflow,
Verified: false,
}
r.SetVerificationError(context.DeadlineExceeded)
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, real secrets, verification error due to unexpected api surface",
s: Scanner{client: common.ConstantResponseHttpClient(500, "{}")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aeroworkflow secret %s within aeroworkflow %s", secret, id)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Aeroworkflow,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 500"))
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aeroworkflow secret %s within aeroworkflow %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Aeroworkflow,
Verified: false,
},
},
wantErr: 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,
},
}
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("Aeroworkflow.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")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Aeroworkflow.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/aeroworkflow/aeroworkflow_test.go
================================================
package aeroworkflow
import (
"context"
"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"
)
func TestAeroWorkflow_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the aeroworkflow API
[DEBUG] Using aeroworkflow KEY=VmFYK7WG3CkgVmTl:c*X
[DEBUG] Using aeroworkflow ID=678436
[INFO] Response received: 200 OK
`,
want: []string{"VmFYK7WG3CkgVmTl:c*X678436"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{aeroworkflow 6}{aeroworkflow AQAAABAAA XjPSUOhREIN:4HX2#akH}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"XjPSUOhREIN:4HX2#akH6"},
},
{
name: "valid pattern - out of prefix range",
input: `
[INFO] Sending request to the aeroworkflow API
[DEBUG] Using KEY=VmFYK7WG3CkgVmTl:c*X
[DEBUG] Using ID=678436
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "valid pattern - only key",
input: `
[INFO] Sending request to the aeroworkflow API
[DEBUG] Using KEY=VmFYK7WG3CkgVmTl:c*X
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "valid pattern - only id",
input: `
[INFO] Sending request to the aeroworkflow API
[DEBUG] Using ID=678436
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the aeroworkflow API
[DEBUG] Using KEY=VmFYK7WG3CkgVmTl:c*X
[DEBUG] Using ID=cxu9w2g6
[ERROR] Response received: 400 BadRequest
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/agora/agora.go
================================================
package agora
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
client *http.Client
}
const agoraURL = "https://api.agora.io"
var (
// Ensure the Scanner satisfies the interface at compile time.
_ detectors.Detector = (*Scanner)(nil)
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"agora", "key", "token"}) + `\b([a-z0-9]{32})\b`)
secretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"agora", "secret"}) + `\b([a-z0-9]{32})\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{"agora"}
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Agora 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
secretMatches := secretPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, secret := range secretMatches {
resSecret := strings.TrimSpace(secret[1])
/*
as both agora key and secretMatch has same regex, the set of strings keyMatch for both probably me same.
we need to avoid the scenario where key is same as secretMatch. This will reduce the number of matches we process.
*/
if resMatch == resSecret {
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Agora,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resSecret),
}
if verify {
client := s.getClient()
isVerified, verificationErr := verifyAgora(ctx, client, resMatch, resSecret)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyAgora(ctx context.Context, client *http.Client, resMatch, resSecret string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, agoraURL+"/dev/v1/projects", nil)
if err != nil {
return false, err
}
req.SetBasicAuth(resSecret, resMatch)
res, err := client.Do(req)
if err != nil {
return false, err
}
defer res.Body.Close()
// https://docs.agora.io/en/voice-calling/reference/agora-console-rest-api#get-all-projects
switch res.StatusCode {
case http.StatusOK, http.StatusCreated:
return true, nil
case http.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Agora
}
func (s Scanner) Description() string {
return "Agora is a real-time engagement platform providing APIs for voice, video, and messaging. Agora API keys can be used to access and manage these services."
}
================================================
FILE: pkg/detectors/agora/agora_integration_test.go
================================================
//go:build detectors
// +build detectors
package agora
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/detectorspb"
)
func TestAgora_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
id := testSecrets.MustGetField("AGORA")
secret := testSecrets.MustGetField("AGORA_SECRET")
inactiveSecret := testSecrets.MustGetField("AGORA_SECRET_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a agora secret %s within agora id %s but verified", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Agora,
Verified: true,
},
{
DetectorType: detectorspb.DetectorType_Agora,
Verified: false,
},
},
wantErr: false,
},
{
name: "found, real secrets, verification error due to timeout",
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a agora secret %s within agora id %s but verified", secret, id)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Agora,
Verified: false,
}
r.SetVerificationError(context.DeadlineExceeded)
return []detectors.Result{r, r}
}(),
wantErr: false,
},
{
name: "found, real secrets, verification error due to unexpected api surface",
s: Scanner{client: common.ConstantResponseHttpClient(500, "{}")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a agora secret %s within agora id %s but verified", secret, id)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Agora,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 500"))
return []detectors.Result{r, r}
}(),
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a agora secret %s within agora id %s but not valid ", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Agora,
Verified: false,
},
{
DetectorType: detectorspb.DetectorType_Agora,
Verified: false,
},
},
wantErr: 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,
},
}
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("Agora.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
for i := range got {
if len(got[i].Raw) == 0 {
t.Fatal("no raw secret present")
}
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")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Agora.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/agora/agora_test.go
================================================
package agora
import (
"context"
"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"
)
func TestAgora_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the agora API
[DEBUG] Using Token=6p77f9gjhxx9mwdj86of7y7820bh49vw
[DEBUG] Using Secret=qi6txx6vd0qzn6j01xj9rr6clyejvjw5
[INFO] Response received: 200 OK
`,
want: []string{"6p77f9gjhxx9mwdj86of7y7820bh49vwqi6txx6vd0qzn6j01xj9rr6clyejvjw5"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{agora 3devtbiys8b282kidr9u78kjq8xdtlo1}{AQAAABAAA bc7c6tag5jfuhz4y7v6v05dx2wq2z1ua}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"3devtbiys8b282kidr9u78kjq8xdtlo1bc7c6tag5jfuhz4y7v6v05dx2wq2z1ua"},
},
{
name: "valid pattern - out of prefix range",
input: `
[INFO] Sending request to the agora API
[DEBUG] Using 6p77f9gjhxx9mwdj86of7y7820bh49vw
[DEBUG] Using qi6txx6vd0qzn6j01xj9rr6clyejvjw5
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "valid pattern - only key",
input: `
[INFO] Sending request to the agora API
[DEBUG] Using Key=6p77f9gjhxx9mwdj86of7y7820bh49vw
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "valid pattern - only secret",
input: `
[INFO] Sending request to the agora API
[DEBUG] Using Secret=qi6txx6vd0qzn6j01xj9rr6clyejvjw5
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the agora API
[DEBUG] Using KEY=qi6txx6vd0qzn6j01xj9rr6clyejvjw
[DEBUG] Using ID=qi6txx6vd0qzn6j01xj9rr6clyejvjw5yt
[ERROR] Response received: 400 BadRequest
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/aha/aha.go
================================================
package aha
import (
"context"
"fmt"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
client *http.Client
}
var (
// Ensure the Scanner satisfies the interface at compile time.
_ detectors.Detector = (*Scanner)(nil)
defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"aha"}) + `\b([0-9a-f]{64})\b`)
URLPat = regexp.MustCompile(`\b([A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])\.aha\.io)`)
)
// 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{"aha.io"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Aha
}
func (s Scanner) Description() string {
return "Aha is a product management software suite. Aha API keys can be used to access and modify product data and workflows."
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Aha 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)
var uniqueFoundUrls = make(map[string]struct{})
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range URLPat.FindAllStringSubmatch(dataStr, -1) {
uniqueFoundUrls[match[1]] = struct{}{}
}
// if no url was found use the default
if len(uniqueFoundUrls) == 0 {
uniqueFoundUrls["aha.io"] = struct{}{}
}
for _, match := range matches {
for url := range uniqueFoundUrls {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Aha,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + url),
}
if verify {
client := s.getClient()
isVerified, verificationErr := verifyAha(ctx, client, resMatch, url)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyAha(ctx context.Context, client *http.Client, resMatch, resURLMatch string) (bool, error) {
url := fmt.Sprintf("https://%s/api/v1/me", resURLMatch)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return false, err
}
req.Header.Add("Accept", "application/vnd.aha+json; version=3")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err != nil {
return false, err
}
defer res.Body.Close()
// https://www.aha.io/api
switch res.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized, http.StatusNotFound, http.StatusForbidden:
// 403 is a known case where an account is inactive bc of a trial ending or payment issue
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
================================================
FILE: pkg/detectors/aha/aha_integration_test.go
================================================
//go:build detectors
// +build detectors
package aha
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/detectorspb"
)
func TestAha_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)
}
domain := testSecrets.MustGetField("AHA_DOMAIN")
secret := testSecrets.MustGetField("AHA_SECRET")
inactiveSecret := testSecrets.MustGetField("AHA_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: Scanner{client: common.ConstantResponseHttpClient(200, "{}")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aha secret %s within %s but verified", secret, domain)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Aha,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, real secrets, verification error due to timeout",
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aha secret %s within %s but verified", secret, domain)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Aha,
Verified: false,
}
r.SetVerificationError(context.DeadlineExceeded)
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, real secrets, verification error due to unexpected api surface",
s: Scanner{client: common.ConstantResponseHttpClient(500, "{}")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aha secret %s within %s but verified", secret, domain)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Aha,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 500"))
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aha secret %s within but not valid domain %s", inactiveSecret, domain)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Aha,
Verified: false,
},
},
wantErr: 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,
},
}
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("Aha.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")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Aha.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/aha/aha_test.go
================================================
package aha
import (
"context"
"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"
)
func TestAha_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] sending request to the aha.io API
[DEBUG] using key = 81a1411a7e276fd88819df3137eb406e0f281f8a8c417947ca4b025890c8541c
[DEBUG] using host = example.aha.io
[INFO] response received: 200 OK
`,
want: []string{"81a1411a7e276fd88819df3137eb406e0f281f8a8c417947ca4b025890c8541cexample.aha.io"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{aha 3af0b286b668d9636fd68076d6c87a333fe285fd41593cfceab36b35606c915a}{AQAAABAAA ACTp3nufSEO791nIReS5udnRVFcG9j6-CqBJogBxo1pbql.aha.io}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"3af0b286b668d9636fd68076d6c87a333fe285fd41593cfceab36b35606c915aACTp3nufSEO791nIReS5udnRVFcG9j6-CqBJogBxo1pbql.aha.io"},
},
{
name: "valid pattern - key out of prefix range",
input: `
[INFO] sending request to the aha.io API
[WARN] Do not commit the secrets
[DEBUG] using key = 81a1411a7e276fd88819df3137eb406e0f281f8a8c417947ca4b025890c8541c
[DEBUG] using host = example.aha.io
[INFO] response received: 200 OK
`,
want: nil,
},
{
name: "valid pattern - only key",
input: `
[INFO] sending request to the aha.io API
[DEBUG] using key = 81a1411a7e276fd88819df3137eb406e0f281f8a8c417947ca4b025890c8541c
[INFO] response received: 200 OK
`,
want: []string{"81a1411a7e276fd88819df3137eb406e0f281f8a8c417947ca4b025890c8541caha.io"},
},
{
name: "valid pattern - only URL",
input: `
[INFO] sending request to the example.aha.io API
[INFO] response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] sending request to the aha.io API
[DEBUG] using key = 81a1411a7e276fd88819df3137eJ406e0f281f8a8c417947ca4b025890c8541c
[DEBUG] using host = 1test.aha.io
[INFO] response received: 200 OK
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/airbrakeprojectkey/airbrakeprojectkey.go
================================================
package airbrakeprojectkey
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"airbrake"}) + `\b([a-zA-Z-0-9]{32})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"airbrake"}) + `\b([0-9]{6})\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{"airbrake"}
}
// FromData will find and optionally verify AirbrakeProjectKey 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)
var uniqueKeys, uniqueIds = make(map[string]struct{}), make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for _, matches := range idPat.FindAllStringSubmatch(dataStr, -1) {
uniqueIds[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
for id := range uniqueIds {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AirbrakeProjectKey,
Raw: []byte(key),
RawV2: []byte(key + id),
}
s1.ExtraData = map[string]string{
"rotation_guide": "https://howtorotate.com/docs/tutorials/airbrake/",
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAirbrakeProjectKey(ctx, client, key, id)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyAirbrakeProjectKey(ctx context.Context, client *http.Client, key string, id string) (bool, error) {
url := "https://api.airbrake.io/api/v4/projects/" + id + "/deploys?key=" + key
payload := strings.NewReader(`{"environment":"production","username":"john","email":"john@smith.com","repository":"https://github.com/airbrake/airbrake","revision":"38748467ea579e7ae64f7815452307c9d05e05c5","version":"v2.0"}`)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, payload)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
// handle according to detector API responses.
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AirbrakeProjectKey
}
func (s Scanner) Description() string {
return "Airbrake is an error and performance monitoring service for web applications. Airbrake project keys can be used to report and track errors in applications."
}
================================================
FILE: pkg/detectors/airbrakeprojectkey/airbrakeprojectkey_integration_test.go
================================================
//go:build detectors
// +build detectors
package airbrakeprojectkey
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAirbrakeProjectKey_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AIRBRAKEPROJECTKEY_TOKEN")
id := testSecrets.MustGetField("AIRBRAKEPROJECTKEY_ID")
inactiveSecret := testSecrets.MustGetField("AIRBRAKEPROJECTKEY_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a airbrake secret %s within airbrake %s but verified", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AirbrakeProjectKey,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a airbrake secret %s within airbrake %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AirbrakeProjectKey,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AirbrakeProjectKey.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("AirbrakeProjectKey.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/airbrakeprojectkey/airbrakeprojectkey_test.go
================================================
package airbrakeprojectkey
import (
"context"
"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"
)
func TestAirBrakeProjectKey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the airbrake API
[DEBUG] Using airbrake Key=7B759RwRR5Txo9pDxXtPNcrTOj0zhvmR
[DEBUG] Using airbrake ID=856019
[INFO] Response received: 200 OK
`,
want: []string{"7B759RwRR5Txo9pDxXtPNcrTOj0zhvmR856019"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{airbrake 691149}{airbrake AQAAABAAA hYNK8PlcGXZ6PXXDFJI89LCjpoM8koTx}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"hYNK8PlcGXZ6PXXDFJI89LCjpoM8koTx691149"},
},
{
name: "valid pattern - key out of prefix range",
input: `
[INFO] airbrake API request handling
[INFO] Sending request to the API
[DEBUG] Using Key=7B759RwRR5Txo9pDxXtPNcrTOj0zhvmR
[DEBUG] Using ID=856019
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "valid pattern - only key",
input: `
[INFO] Sending request to the airbrake API
[DEBUG] Using airbrake Key=7B759RwRR5Txo9pDxXtPNcrTOj0zhvmR
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "valid pattern - only ID",
input: `
[INFO] Sending request to the airbrake API
[DEBUG] Using airbrake ID=856019
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the airbrake API
[DEBUG] Using airbrake Key=qwmnerBv56zx**cvkjqr78afvYU$90Op
[DEBUG] Using airbrake ID=856019
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/airbrakeuserkey/airbrakeuserkey.go
================================================
package airbrakeuserkey
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"airbrake"}) + `\b([a-zA-Z-0-9]{40})\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{"airbrake"}
}
// FromData will find and optionally verify AirbrakeUserKey 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AirbrakeUserKey,
Raw: []byte(key),
ExtraData: map[string]string{
"rotation_guide": "https://howtorotate.com/docs/tutorials/airbrake/",
},
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAirbrakeUserKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyAirbrakeUserKey(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.airbrake.io/api/v4/projects?key="+key, nil)
if err != nil {
return false, err
}
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AirbrakeUserKey
}
func (s Scanner) Description() string {
return "Airbrake is an error and performance monitoring service. Airbrake User Keys can be used to access and manage error reports and performance data."
}
================================================
FILE: pkg/detectors/airbrakeuserkey/airbrakeuserkey_integration_test.go
================================================
//go:build detectors
// +build detectors
package airbrakeuserkey
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAirbrakeUserKey_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AIRBRAKEUSERKEY_TOKEN")
inactiveSecret := testSecrets.MustGetField("AIRBRAKEUSERKEY_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a airbrakeuserkey secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AirbrakeUserKey,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a airbrakeuserkey secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AirbrakeUserKey,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AirbrakeUserKey.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("AirbrakeUserKey.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/airbrakeuserkey/airbrakeuserkey_test.go
================================================
package airbrakeuserkey
import (
"context"
"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"
)
func TestAirBrakeUserKey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the airbrake API
[DEBUG] Using Key=qsCGuilpkk2ngrsz75wtYqsCGuilpkk2ngrsz75w
[INFO] Response received: 200 OK
`,
want: []string{"qsCGuilpkk2ngrsz75wtYqsCGuilpkk2ngrsz75w"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{airbrake 691149}{airbrake AQAAABAAA UTDwMhGhuk0T04V0yqTqcKIwSSp7syUyQRG8JwoF}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"UTDwMhGhuk0T04V0yqTqcKIwSSp7syUyQRG8JwoF"},
},
{
name: "valid pattern - key out of prefix range",
input: `
[DEBUG] airbrake api processing
[INFO] Sending request to the API
[DEBUG] Using Key=qsCGuilpkk2ngrsz75wtYqsCGuilpkk2ngrsz75w
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the airbrake API
[DEBUG] Using airbrake Key=Qs%CGuil#pkk2ngrsz75wtYqsCGuilpkk2ngrsz75w
[INFO] Response received: 200 OK
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/airship/airship.go
================================================
package airship
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"airship"}) + `\b([0-9a-zA-Z]{91})\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{"airship"}
}
// FromData will find and optionally verify Airship 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Airship,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAirshipKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyAirshipKey(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://go.urbanairship.com/api/schedules", nil)
if err != nil {
return false, err
}
req.Header.Add("Accept", "application/vnd.urbanairship+json; version=3")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Airship
}
func (s Scanner) Description() string {
return "Airship is a customer engagement platform that provides tools for mobile app messaging, in-app messaging, and web notifications. Airship API keys can be used to access and manage these messaging services."
}
================================================
FILE: pkg/detectors/airship/airship_integration_test.go
================================================
//go:build detectors
// +build detectors
package airship
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAirship_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AIRSHIP")
inactiveSecret := testSecrets.MustGetField("AIRSHIP_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a airship secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Airship,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a airship secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Airship,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Airship.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Airship.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/airship/airship_test.go
================================================
package airship
import (
"context"
"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"
)
func TestAirship_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the airship API
[DEBUG] Using Key=O3BV99CUDw3xYUAL0tHGYUe7mOj5PA5vTnLdJwULCTh9dxk9PmmTpL1kI846G3QGIsECVyVSsxZnIbfSwWc8xuX843W
[INFO] Response received: 200 OK
`,
want: []string{"O3BV99CUDw3xYUAL0tHGYUe7mOj5PA5vTnLdJwULCTh9dxk9PmmTpL1kI846G3QGIsECVyVSsxZnIbfSwWc8xuX843W"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{airship}{airship AQAAABAAA oVH3yIO1oAoXpK9Rc01EGNNTuw6d4Zyt07YNFmje644Ht00hvAaYwldNOV9vIPQw6dYHJLRgp2f75YdJ9OiICkYVhMI}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"oVH3yIO1oAoXpK9Rc01EGNNTuw6d4Zyt07YNFmje644Ht00hvAaYwldNOV9vIPQw6dYHJLRgp2f75YdJ9OiICkYVhMI"},
},
{
name: "valid pattern - key out of prefix range",
input: `
[DEBUG] airship api processing
[INFO] Sending request to the API
[DEBUG] Using Key=O3BV99CUDw3xYUAL0tHGYUe7mOj5PA5vTnLdJwULCTh9dxk9PmmTpL1kI846G3QGIsECVyVSsxZnIbfSwWc8xuX843W
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the airship API
[DEBUG] Using Key=O3BV99CUDw3xY#AL0tHGYUe7mOj5PA5vTnLdJwULCTh9dxk9PmmTpL1kI846G3QGIsECVyVSsxZnIbfSwWc8xuX843W
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/airtableoauth/airtableoauth.go
================================================
package airtableoauth
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// The detector will attempt to match access tokens generated through the Airtable OAuth flow
// Airtable OAuth does not support generating access tokens using client ID and key
// Reference: https://airtable.com/developers/web/api/oauth-reference
tokenPat = regexp.MustCompile(`\b([[:alnum:]]+\.v1\.[a-zA-Z0-9_-]+\.[a-f0-9]+)\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{"airtable"}
}
// FromData will find and optionally verify AirtableOAuth 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 tokenPat.FindAllStringSubmatch(dataStr, -1) {
uniqueMatches[match[1]] = struct{}{}
}
for match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AirtableOAuth,
Raw: []byte(match),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, extraData, verificationErr := verifyMatch(ctx, client, match)
s1.Verified = isVerified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr, match)
if s1.Verified {
s1.AnalysisInfo = map[string]string{"token": match}
}
}
results = append(results, s1)
}
return
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, map[string]string, error) {
endpoint := "https://api.airtable.com/v0/meta/whoami"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return false, nil, err
}
req.Header.Add("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()
}()
switch res.StatusCode {
case http.StatusOK:
return true, nil, nil
case http.StatusUnauthorized:
// The secret is determinately not verified (nothing to do)
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AirtableOAuth
}
func (s Scanner) Description() string {
return "Airtable is a cloud collaboration service that offers database-like features. Airtable OAuth tokens can be used to access and modify data within Airtable bases."
}
================================================
FILE: pkg/detectors/airtableoauth/airtableoauth_integration_test.go
================================================
//go:build detectors
// +build detectors
package airtableoauth
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/detectorspb"
)
// TestAirtableoauth_FromChunk verifies the validity of an Airtable OAuth token
// Note: The token validity test relies on an access token stored in the GCP secret manager.
// Since Airtable OAuth tokens expire after 60 minutes, this test will eventually fail once the token becomes invalid.
// The official guide linked below can be followed in order to generate a new valid access token:
// https://airtable.com/developers/web/api/oauth-reference
func TestAirtableoauth_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("AIRTABLEOAUTH")
inactiveSecret := testSecrets.MustGetField("AIRTABLEOAUTH_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a airtableoauth secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AirtableOAuth,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a airtableoauth secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AirtableOAuth,
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 airtableoauth secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AirtableOAuth,
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 airtableoauth secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AirtableOAuth,
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("Airtableoauth.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Airtableoauth.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/airtableoauth/airtableoauth_test.go
================================================
package airtableoauth
import (
"context"
"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"
)
func TestAirtableoauth_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the airtable API
[DEBUG] Using Key=oaajtCy2lVMUN1Cm5.v1.eyJ1c2VySWQiOiJ1c3JjQ09QVlJudGlrU1lzdyIsImV4cGlyZXNBdCI6IjIwMjUtMDItMDNUMTk6NTY6MzcuMDAwWiIsIm9hdXRoQXBwbGljYXRpb25JZCI6Im9hcG14aXcyUlRrVGlzcHJIIiwic2VjcmV0IjoiMzczNThlNzdlZjlhMjljY2Q5MWIwNmNlNTdkZDYxNDg0MWVmNmIyOWYwYjQ5ZWE0MTMxZGI4NzBkNTAzYTE1NyJ9.0d67c8b334048135a93615610445e4aa90c6af6222392b49eea9419e1d6717d0
[INFO] Response received: 200 OK
`,
want: []string{"oaajtCy2lVMUN1Cm5.v1.eyJ1c2VySWQiOiJ1c3JjQ09QVlJudGlrU1lzdyIsImV4cGlyZXNBdCI6IjIwMjUtMDItMDNUMTk6NTY6MzcuMDAwWiIsIm9hdXRoQXBwbGljYXRpb25JZCI6Im9hcG14aXcyUlRrVGlzcHJIIiwic2VjcmV0IjoiMzczNThlNzdlZjlhMjljY2Q5MWIwNmNlNTdkZDYxNDg0MWVmNmIyOWYwYjQ5ZWE0MTMxZGI4NzBkNTAzYTE1NyJ9.0d67c8b334048135a93615610445e4aa90c6af6222392b49eea9419e1d6717d0"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{airtable}{airtable AQAAABAAA iKMJv6D1mmUvunFTZLfm4RrYhdrt5JCBMv.v1.r8IBnGw7b_vW0fl0MDJqPRUEsDdHtNYW9ANwPFm40V_M4knoEaulKL-5lmtWoRq9fjG-GORe8efob5e658nTiOkdYC.8a8d3}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"iKMJv6D1mmUvunFTZLfm4RrYhdrt5JCBMv.v1.r8IBnGw7b_vW0fl0MDJqPRUEsDdHtNYW9ANwPFm40V_M4knoEaulKL-5lmtWoRq9fjG-GORe8efob5e658nTiOkdYC.8a8d3"},
},
{
name: "finds all matches",
input: `
[INFO] Sending request to the airtable API
[DEBUG] Using Key=oaajtCy2lVMUN1Cm5.v1.eyJ1c2VySWQiOiJ1c3JjQ09QVlJudGlrU1lzdyIsImV4cGlyZXNBdCI6IjIwMjUtMDItMDNUMTk6NTY6MzcuMDAwWiIsIm9hdXRoQXBwbGljYXRpb25JZCI6Im9hcG14aXcyUlRrVGlzcHJIIiwic2VjcmV0IjoiMzczNThlNzdlZjlhMjljY2Q5MWIwNmNlNTdkZDYxNDg0MWVmNmIyOWYwYjQ5ZWE0MTMxZGI4NzBkNTAzYTE1NyJ9.0d67c8b334048135a93615610445e4aa90c6af6222392b49eea9419e1d6717d0
[ERROR] Response received: 401 UnAuthorized
[DEBUG] Using Key=oaaRYiYSlTFXZzxDM.v1.eyJ1c2VySWQiOiJ1c3JjQ09QVlJudGlrU1lzdyIsIm9hdXRoQXBwbGljYXRpb25JZCI6Im9hcG14aXcyUlRrVGlzcHJIIiwiZXhwaXJlc0F0IjoiMjAyNS0wMS0yOVQwMDowMTo0NC4wMDBaIiwic2VjcmV0IjoiZjYyOWE1MWVkM2M0ZjU5ODlmOTcyMDU1ZjkwODk3NDA4NmU0NjQxY2JhODU5Y2FhZTJkZjliMWQwODg0ZjIzMiJ9.27a8998029ac9bdd599b435572821dcb63c60cbf62b9cb2ba2a73511e5553d66
[INFO] Response received: 200 OK
`,
want: []string{
"oaajtCy2lVMUN1Cm5.v1.eyJ1c2VySWQiOiJ1c3JjQ09QVlJudGlrU1lzdyIsImV4cGlyZXNBdCI6IjIwMjUtMDItMDNUMTk6NTY6MzcuMDAwWiIsIm9hdXRoQXBwbGljYXRpb25JZCI6Im9hcG14aXcyUlRrVGlzcHJIIiwic2VjcmV0IjoiMzczNThlNzdlZjlhMjljY2Q5MWIwNmNlNTdkZDYxNDg0MWVmNmIyOWYwYjQ5ZWE0MTMxZGI4NzBkNTAzYTE1NyJ9.0d67c8b334048135a93615610445e4aa90c6af6222392b49eea9419e1d6717d0",
"oaaRYiYSlTFXZzxDM.v1.eyJ1c2VySWQiOiJ1c3JjQ09QVlJudGlrU1lzdyIsIm9hdXRoQXBwbGljYXRpb25JZCI6Im9hcG14aXcyUlRrVGlzcHJIIiwiZXhwaXJlc0F0IjoiMjAyNS0wMS0yOVQwMDowMTo0NC4wMDBaIiwic2VjcmV0IjoiZjYyOWE1MWVkM2M0ZjU5ODlmOTcyMDU1ZjkwODk3NDA4NmU0NjQxY2JhODU5Y2FhZTJkZjliMWQwODg0ZjIzMiJ9.27a8998029ac9bdd599b435572821dcb63c60cbf62b9cb2ba2a73511e5553d66",
},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the airtable API
[DEBUG] Using Key=oaaRYiYSlTFXZzxDM.v2.eyJ1c2VySWQiOiJ1c3JjQ09QVlJudGlrU1lzdyIsIm9hdXRoQXBwbGljYXRpb25JZCI6Im9hcG14aXcyUlRrVGlzcHJIIiwiZXhwaXJlc0F0IjoiMjAyNS0wMS0yOVQwMDowMTo0NC4wMDBaIiwic2VjcmV0IjoiZjYyOWE1MWVkM2M0ZjU5ODlmOTcyMDU1ZjkwODk3NDA4NmU0NjQxY2JhODU5Y2FhZTJkZjliMWQwODg0ZjIzMiJ9.27a8998029ac9bdd599b435572821dcb63c60cbf62b9cb2ba2a73511e5553d66
[ERROR] Response received: 401 UnAuthorized
`,
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("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/airtablepersonalaccesstoken/airtablepersonalaccesstoken.go
================================================
package airtablepersonalaccesstoken
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
tokenPat = regexp.MustCompile(detectors.PrefixRegex([]string{"airtable"}) + `\b(pat[[:alnum:]]{14}\.[a-f0-9]{64})\b`)
)
func (s Scanner) Keywords() []string {
return []string{"airtable"}
}
// FromData will find and optionally verify AirtableOAuth 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 tokenPat.FindAllStringSubmatch(dataStr, -1) {
uniqueMatches[match[1]] = struct{}{}
}
for match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AirtablePersonalAccessToken,
Raw: []byte(match),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, extraData, verificationErr := verifyMatch(ctx, client, match)
s1.Verified = isVerified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr, match)
if s1.Verified {
s1.AnalysisInfo = map[string]string{"token": match}
}
}
results = append(results, s1)
}
return
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, map[string]string, error) {
endpoint := "https://api.airtable.com/v0/meta/whoami"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return false, nil, err
}
req.Header.Add("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()
}()
switch res.StatusCode {
case http.StatusOK:
return true, nil, nil
case http.StatusUnauthorized:
// The secret is determinately not verified (nothing to do)
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AirtablePersonalAccessToken
}
func (s Scanner) Description() string {
return "Airtable is a cloud collaboration service that offers database-like features. Airtable OAuth tokens can be used to access and modify data within Airtable bases."
}
================================================
FILE: pkg/detectors/airtablepersonalaccesstoken/airtablepersonalaccesstoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package airtablepersonalaccesstoken
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/detectorspb"
)
func TestAirtablepersonalaccesstoken_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("AIRTABLEPERSONALACCESSTOKEN")
inactiveSecret := testSecrets.MustGetField("AIRTABLEPERSONALACCESSTOKEN_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find an airtable secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AirtablePersonalAccessToken,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find an airtable secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AirtablePersonalAccessToken,
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 airtablepersonalaccesstoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AirtablePersonalAccessToken,
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 airtablepersonalaccesstoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AirtablePersonalAccessToken,
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("Airtablepersonalaccesstoken.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Airtablepersonalaccesstoken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/airtablepersonalaccesstoken/airtablepersonalaccesstoken_test.go
================================================
package airtablepersonalaccesstoken
import (
"context"
"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"
)
func TestAirtablepersonalaccesstoken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the airtable API
[DEBUG] Using Key=patfqpIZBPU6EAt5x.458546d9c77b21f8a98141f2a4039d5626010f19efc16c20d57c4f41d44c8c85
[INFO] Response received: 200 OK
`,
want: []string{"patfqpIZBPU6EAt5x.458546d9c77b21f8a98141f2a4039d5626010f19efc16c20d57c4f41d44c8c85"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{airtable}{airtable AQAAABAAA pat2kATFGrujqJTbT.e2082656c470902d83b47dc804e693df1deb30161affbda39d879a2cf44bef13}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"pat2kATFGrujqJTbT.e2082656c470902d83b47dc804e693df1deb30161affbda39d879a2cf44bef13"},
},
{
name: "finds all matches",
input: `
[INFO] Sending request to the API
[DEBUG] Using airtable Key=patfqpIZBPU6EAt5x.458546d9c77b21f8a98141f2a4039d5626010f19efc16c20d57c4f41d44c8c85
[ERROR] Response received: 401 UnAuthorized
[DEBUG] Using airtable Key=pat0VXr5I2HcapZE8.da2606afb7d97e936719ec952a4a18b44045e385d4ddf4f38dcc246fb63f0165
[INFO] Response received: 200 OK
`,
want: []string{
"patfqpIZBPU6EAt5x.458546d9c77b21f8a98141f2a4039d5626010f19efc16c20d57c4f41d44c8c85",
"pat0VXr5I2HcapZE8.da2606afb7d97e936719ec952a4a18b44045e385d4ddf4f38dcc246fb63f0165",
},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the airtable API
[DEBUG] Using Key=patfqpIZBPU6EAt5xe.458546d9c77b21f8a98141f2a403-d5626010f19efc16c20d57c4f41d44c8c85
[ERROR] Response received: 401 UnAuthorized
`,
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("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/airvisual/airvisual.go
================================================
package airvisual
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"airvisual"}) + `\b([a-z0-9-]{36})\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{"airvisual"}
}
// FromData will find and optionally verify AirVisual 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AirVisual,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAirVisualKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyAirVisualKey(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://api.airvisual.com/v2/countries?key=%s", key), nil)
if err != nil {
return false, err
}
req.Header.Add("Accept", "application/vnd.airvisual+json; version=3")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AirVisual
}
func (s Scanner) Description() string {
return "AirVisual provides air quality information and monitoring. The API key allows access to various air quality data and services."
}
================================================
FILE: pkg/detectors/airvisual/airvisual_integration_test.go
================================================
//go:build detectors
// +build detectors
package airvisual
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAirVisual_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AIRVISUAL")
inactiveSecret := testSecrets.MustGetField("AIRVISUAL_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a airvisual secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AirVisual,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a airvisual secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AirVisual,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AirVisual.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("AirVisual.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/airvisual/airvisual_test.go
================================================
package airvisual
import (
"context"
"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"
)
func TestAirVisual_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the airvisual API
[DEBUG] Using Key=qscgyygcsq-wdvvok7slklklaasnd8afafxd
[INFO] Response received: 200 OK
`,
want: []string{"qscgyygcsq-wdvvok7slklklaasnd8afafxd"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{airvisual}{airvisual AQAAABAAA rtcbsxiee3d5au8ik14g-8iqrsu8thl1pku8}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"rtcbsxiee3d5au8ik14g-8iqrsu8thl1pku8"},
},
{
name: "valid pattern - key out of prefix range",
input: `
[DEBUG] airvisual api processing
[INFO] Sending request to the API
[DEBUG] Using Key=qscgyygcsq-wdvvok7slklklaasnd8afafxd
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the airvisual API
[DEBUG] Using Key=wdvvok7slklklaasnd8afafxd
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/aiven/aiven.go
================================================
package aiven
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"aiven"}) + `([a-zA-Z0-9/+=]{372})`)
)
// 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{"aiven"}
}
// FromData will find and optionally verify Aiven 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Aiven,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAivenKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyAivenKey(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.aiven.io/v1/project", nil)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("aivenv1 %s", key))
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Aiven
}
func (s Scanner) Description() string {
return "Aiven is a managed cloud service that provides various open-source data infrastructure services. Aiven API keys can be used to access and manage these services."
}
================================================
FILE: pkg/detectors/aiven/aiven_integration_test.go
================================================
//go:build detectors
// +build detectors
package aiven
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAiven_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AIVEN")
inactiveSecret := testSecrets.MustGetField("AIVEN_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aiven secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Aiven,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aiven secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Aiven,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Aiven.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Aiven.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/aiven/aiven_test.go
================================================
package aiven
import (
"context"
"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"
)
func TestAiven_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the aiven API
[DEBUG] Using Key = yb+Ygm82FfUworm2exB+Uk255p0uQKmmfx4ut1KfsZ3YI3Gp2xPYyxZgrwYabMxXXO4WPsK7xlLJRy0BWIpM2SKnzA2p69P8aOmYbl24ZiVGlLXyQxeVDDy7gru5Yzt=Y1UDLBpsW=hhGIKsrPgc/7hpxuEfEqbXJe5IBYO484F+ekaTmYN4nTF94O==3WuG+WuSW7zaYzXH1V==kZFj07zBtmShS0z/lW=N3HipH=oJjXI2pyFxU+A7vM9yHdUHoiZEOVoWsyp5zO1ajBOqFr=3jIIaXWmbH33dP2ZNQFJhqbeg6JlXA9GpfMFht5=ZCC1IirWCNp=UILbmZtvu9d2M8U0YNHwAGKtjrPS5lZvAU+W5s2Ti
[INFO] Response received: 200 OK
`,
want: []string{"yb+Ygm82FfUworm2exB+Uk255p0uQKmmfx4ut1KfsZ3YI3Gp2xPYyxZgrwYabMxXXO4WPsK7xlLJRy0BWIpM2SKnzA2p69P8aOmYbl24ZiVGlLXyQxeVDDy7gru5Yzt=Y1UDLBpsW=hhGIKsrPgc/7hpxuEfEqbXJe5IBYO484F+ekaTmYN4nTF94O==3WuG+WuSW7zaYzXH1V==kZFj07zBtmShS0z/lW=N3HipH=oJjXI2pyFxU+A7vM9yHdUHoiZEOVoWsyp5zO1ajBOqFr=3jIIaXWmbH33dP2ZNQFJhqbeg6JlXA9GpfMFht5=ZCC1IirWCNp=UILbmZtvu9d2M8U0YNHwAGKtjrPS5lZvAU+W5s2Ti"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{aiven}{aiven AQAAABAAA IGhXNR6g7rogABp/H2iDQu7TgkXpvn9KnwzJfeh+8p7M=JVsI2QoQ38mmQHt450bQC4wBOGFhV+9QT2KGWSMfTOxTUrUXygaLlwsXo/RBxKXyOdh=/L8EGGrqG6=qbd0UzDAfc0xeAfXd30RGj+Ypsrrvdda=ZPa32BBID5r2ClfJSbgpfWIpVC1b5vlqCdy5LIWABZJzjBC5VweqZ04XFaCh+15NuSQ4E0KdGwPdkrfxxjY20I1wDvlKxzxL7dfCly3KVlQv7KBEFSLaLRNRocPYToUXqU4yAXKvXf03K=k1mahpxFUp94c35k/n055LVs=xbyL6AKdW=sCCa1AFIYKBDMBprTsZ6Al7DHx=XA6qLNWYxS7}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"IGhXNR6g7rogABp/H2iDQu7TgkXpvn9KnwzJfeh+8p7M=JVsI2QoQ38mmQHt450bQC4wBOGFhV+9QT2KGWSMfTOxTUrUXygaLlwsXo/RBxKXyOdh=/L8EGGrqG6=qbd0UzDAfc0xeAfXd30RGj+Ypsrrvdda=ZPa32BBID5r2ClfJSbgpfWIpVC1b5vlqCdy5LIWABZJzjBC5VweqZ04XFaCh+15NuSQ4E0KdGwPdkrfxxjY20I1wDvlKxzxL7dfCly3KVlQv7KBEFSLaLRNRocPYToUXqU4yAXKvXf03K=k1mahpxFUp94c35k/n055LVs=xbyL6AKdW=sCCa1AFIYKBDMBprTsZ6Al7DHx=XA6qLNWYxS7"},
},
{
name: "valid pattern - key out of prefix range",
input: `
[DEBUG] aiven api processing
[INFO] Sending request to the API
[DEBUG] Using Key=yb+Ygm82FfUworm2exB+Uk255p0uQKmmfx4ut1KfsZ3YI3Gp2xPYyxZgrwYabMxXXO4WPsK7xlLJRy0BWIpM2SKnzA2p69P8aOmYbl24ZiVGlLXyQxeVDDy7gru5Yzt=Y1UDLBpsW=hhGIKsrPgc/7hpxuEfEqbXJe5IBYO484F+ekaTmYN4nTF94O==3WuG+WuSW7zaYzXH1V==kZFj07zBtmShS0z/lW=N3HipH=oJjXI2pyFxU+A7vM9yHdUHoiZEOVoWsyp5zO1ajBOqFr=3jIIaXWmbH33dP2ZNQFJhqbeg6JlXA9GpfMFht5=ZCC1IirWCNp=UILbmZtvu9d2M8U0YNHwAGKtjrPS5lZvAU+W5s2Ti
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the aiven API
[DEBUG] Using Key=SSs8PGhwqWzb4qfqiwLV/bNHfiQ2VSKyX88AAYm3+CGHbTe/FYXRNOYYHO=PXwuL/GftiES7j8ffzWW9p1dAyNc6hZZpoazmd+Vf1kbukZSL8QO/LdKFI/YFlupu0dELqQVHeZi/cJlnp6aQeY7zIJiHhJS51ZVdOamc=zOUMebry3BYOo2LhYIz+mLND7s5/cHZZpkEvTXrKnVf4vdYMl+fawv84AYCTo9pry8FQBsqRex2HL98kAiqhVYG+nLyRz/hZCo8owaRkzli1BUT4O63TSKJIgnECOBvyZz7o+yX92BhDe+B2Tllk3y2=qG5TiEl2sCJI8V5GJ1cz52RpXx2hVXMi=1Zl5CHpX8Adr9VMbj$Co
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/alchemy/alchemy.go
================================================
package alchemy
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"alchemy"}) + `\b([0-9a-zA-Z_]{32}|alcht_[0-9a-zA-Z]{30})\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{"alchemy", "alcht_"}
}
// FromData will find and optionally verify Alchemy 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 match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Alchemy,
Raw: []byte(match),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, extraData, verificationErr := verifyMatch(ctx, client, match)
s1.Verified = isVerified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr, match)
}
results = append(results, s1)
}
return
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, map[string]string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://eth-mainnet.g.alchemy.com/v2/"+token+"/getNFTs/?owner=vitalik.eth", nil)
if err != nil {
return false, nil, err
}
res, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
// If the endpoint returns useful information, we can return it as a map.
return true, nil, nil
case http.StatusUnauthorized:
// The secret is determinately not verified (nothing to do)
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Alchemy
}
func (s Scanner) Description() string {
return "Alchemy is a blockchain development platform that provides a suite of tools and services for building and scaling decentralized applications. Alchemy API keys can be used to access these services."
}
================================================
FILE: pkg/detectors/alchemy/alchemy_integration_test.go
================================================
//go:build detectors
// +build detectors
package alchemy
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/detectorspb"
)
func TestAlchemy_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("ALCHEMY")
inactiveSecret := testSecrets.MustGetField("ALCHEMY_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a alchemy secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Alchemy,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a alchemy secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Alchemy,
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 alchemy secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Alchemy,
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 alchemy secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Alchemy,
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("Alchemy.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Alchemy.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/alchemy/alchemy_test.go
================================================
package alchemy
import (
"context"
"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"
)
func TestAlchemy_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the alchemy API
[DEBUG] Using Key=alcht_2Cy8xCLyvrAf7lZKfhQhyCr4RAID9D
[INFO] Response received: 200 OK
`,
want: []string{"alcht_2Cy8xCLyvrAf7lZKfhQhyCr4RAID9D"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{alchemy}{alchemy AQAAABAAA 5iqW7gKQVXvwnykF9xAVfenemmnUJznI}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"5iqW7gKQVXvwnykF9xAVfenemmnUJznI"},
},
{
name: "finds all matches",
input: `
[INFO] Sending request to the alchemy API
[DEBUG] Using Key=alcht_2Cy8xCLyvrAf7lZKfhQhyCr4RAID9D
[ERROR] Response received 401 UnAuthorized
[DEBUG] Using alchemy Key=xuQIeWFVEp8k8Uu9FwPx6X5C8IViOe1o
[INFO] Response received: 200 OK
`,
want: []string{"alcht_2Cy8xCLyvrAf7lZKfhQhyCr4RAID9D", "xuQIeWFVEp8k8Uu9FwPx6X5C8IViOe1o"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the alchemy API
[DEBUG] Using Key=alcht_a2Cy8xCLyvrAf7lZKfhQhyCr4RAID9D
[ERROR] Response received: 401 UnAuthorized
`,
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("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/alconost/alconost.go
================================================
package alconost
import (
"context"
b64 "encoding/base64"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"alconost"}) + `\b([0-9Aa-z]{32})\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{"alconost"}
}
// FromData will find and optionally verify Alconost 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Alconost,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAlconostKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyAlconostKey(ctx context.Context, client *http.Client, key string) (bool, error) {
data := fmt.Sprintf("%s:", key)
sEnc := b64.StdEncoding.EncodeToString([]byte(data))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://nitro.alconost.com/api/v1/account", nil)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Alconost
}
func (s Scanner) Description() string {
return "Alconost is a translation and localization service. Alconost API keys can be used to access and modify translation data."
}
================================================
FILE: pkg/detectors/alconost/alconost_integration_test.go
================================================
//go:build detectors
// +build detectors
package alconost
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAlconost_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ALCONOST")
inactiveSecret := testSecrets.MustGetField("ALCONOST_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a alconost secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Alconost,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a alconost secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Alconost,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Alconost.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Alconost.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/alconost/alconost_test.go
================================================
package alconost
import (
"context"
"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"
)
func TestAlconost_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the alconost API
[DEBUG] Using Key=wdvnousa87acfxp9ioasrea4tbeasrfa
[INFO] Response received: 200 OK
`,
want: []string{"wdvnousa87acfxp9ioasrea4tbeasrfa"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{alconost}{alconost AQAAABAAA Awxzhkwff46dtkt5pnvdlss6t2kA44a7}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"Awxzhkwff46dtkt5pnvdlss6t2kA44a7"},
},
{
name: "valid pattern - key out of prefix range",
input: `
[DEBUG] alconost api processing
[INFO] Sending request to the API
[DEBUG] Using Key=wdvnousa87acfxp9ioasrea4tbeasrfa
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the alconost API
[DEBUG] Using Key=wdvnousa87acfxp9ioasra4tBeasrfa
[INFO] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/alegra/alegra.go
================================================
package alegra
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"alegra"}) + `\b([a-z0-9-]{20})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"alegra"}) + common.EmailPattern)
)
// 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{"alegra"}
}
// FromData will find and optionally verify Alegra 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)
keyMatches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
uniqueTokens := make(map[string]struct{})
uniqueIDs := make(map[string]struct{})
for _, match := range keyMatches {
uniqueTokens[match[1]] = struct{}{}
}
for _, match := range idMatches {
id := match[0][strings.LastIndex(match[0], " ")+1:]
uniqueIDs[id] = struct{}{}
}
for token := range uniqueTokens {
for id := range uniqueIDs {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Alegra,
Raw: []byte(token),
RawV2: []byte(token + ":" + id),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyCredentials(ctx, client, id, token)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, token)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyCredentials(ctx context.Context, client *http.Client, username, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.alegra.com/api/v1/users/self", nil)
if err != nil {
return false, nil
}
req.SetBasicAuth(username, 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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Alegra
}
func (s Scanner) Description() string {
return "Alegra is a cloud-based accounting software. Alegra API keys can be used to access and modify accounting data and user information."
}
================================================
FILE: pkg/detectors/alegra/alegra_integration_test.go
================================================
//go:build detectors
// +build detectors
package alegra
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAlegra_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ALEGRA")
id := testSecrets.MustGetField("ACCOUNT_USER")
inactiveSecret := testSecrets.MustGetField("ALEGRA_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a alegra secret %s within alegra %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Alegra,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a alegra secret %s within alegra %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Alegra,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Alegra.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])
}
got[i].Raw = nil
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Alegra.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/alegra/alegra_test.go
================================================
package alegra
import (
"context"
"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"
)
func TestAlegra_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using alegra Key=wdvn-usa87a-fxp9ioas
[DEBUG] Using alegra Email = testUser.1005@example.com
[INFO] Response received: 200 OK
`,
want: []string{"wdvn-usa87a-fxp9ioas:testUser.1005@example.com"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{alegra kk18@example.com}{alegra AQAAABAAA buihlmkfnh5m1lk5z6do}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"buihlmkfnh5m1lk5z6do:kk18@example.com"},
},
{
name: "valid pattern - key out of prefix range",
input: `
[DEBUG] alegra api processing
[INFO] Sending request to the API
[DEBUG] Using Key=wdvn-usa87a-fxp9ioas
[DEBUG] Using Email=testUser.1005@example.com
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using alegra Key=wdvn_usa87a-fxp9ioas
[DEBUG] Using alegra Email=testUser.1005@example.com
[INFO] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/aletheiaapi/aletheiaapi.go
================================================
package aletheiaapi
import (
"context"
"fmt"
"io"
"net/http"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"aletheiaapi"}) + `\b([A-Z0-9]{32})\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{"aletheiaapi"}
}
// FromData will find and optionally verify AletheiaApi 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AletheiaApi,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAletheiaAPIKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyAletheiaAPIKey(ctx context.Context, client *http.Client, key string) (bool, error) {
timeout := 10 * time.Second
client.Timeout = timeout
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.aletheiaapi.com/StockData?symbol=msft&summary=true", nil)
if err != nil {
return false, err
}
req.Header.Add("Key", key)
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AletheiaApi
}
func (s Scanner) Description() string {
return "AletheiaApi is a service providing financial data. AletheiaApi keys can be used to access this data."
}
================================================
FILE: pkg/detectors/aletheiaapi/aletheiaapi_integration_test.go
================================================
//go:build detectors
// +build detectors
package aletheiaapi
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAletheiaApi_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ALETHEIAAPI")
inactiveSecret := testSecrets.MustGetField("ALETHEIAAPI_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aletheiaapi secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AletheiaApi,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aletheiaapi secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AletheiaApi,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AletheiaApi.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("AletheiaApi.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/aletheiaapi/aletheiaapi_test.go
================================================
package aletheiaapi
import (
"context"
"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"
)
func TestAleTheIaAPI_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the aletheiaapi
[DEBUG] Using Key=LY027C40U2KNNZLFO1WEU3XQZ13LW515
[INFO] Response received: 200 OK
`,
want: []string{"LY027C40U2KNNZLFO1WEU3XQZ13LW515"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{aletheiaapi}{aletheiaapi AQAAABAAA K7SOW2B8QH9QE435NLH07PH22XL4YOPG}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"K7SOW2B8QH9QE435NLH07PH22XL4YOPG"},
},
{
name: "valid pattern - key out of prefix range",
input: `
[DEBUG] aletheiaapi api processing
[INFO] Sending request to the API
[DEBUG] Using Key=LY027C40U2KNNZLFO1WEU3XQZ13LW515
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the aletheiaapi
[DEBUG] Using Key=LY027c40U2KNNZLFO1WEU3XQZ13LW515
[INFO] Response received: 200 OK
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/algoliaadminkey/algoliaadminkey.go
================================================
package algoliaadminkey
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"slices"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"algolia", "docsearch", "appId"}) + `\b([A-Z0-9]{10})\b`)
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"algolia", "docsearch", "apiKey"}) + `\b([a-zA-Z0-9]{32})\b`)
invalidHosts = simple.NewCache[struct{}]()
errNoHost = errors.New("no such host")
)
// 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{"algolia", "docsearch"}
}
// FromData will find and optionally verify AlgoliaAdminKey secrets in a given set of bytes.
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
logger := logContext.AddLogger(ctx).Logger().WithName("algoliaadminkey")
dataStr := string(data)
// Deduplicate matches.
idMatches := make(map[string]struct{})
for _, match := range idPat.FindAllStringSubmatch(dataStr, -1) {
id := match[1]
if detectors.StringShannonEntropy(id) > 2 {
idMatches[id] = struct{}{}
}
}
keyMatches := make(map[string]struct{})
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
key := match[1]
if detectors.StringShannonEntropy(key) > 3 {
keyMatches[key] = struct{}{}
}
}
// Test matches.
for key := range keyMatches {
for id := range idMatches {
if invalidHosts.Exists(id) {
logger.V(3).Info("Skipping application id: no such host", "host", id)
delete(idMatches, id)
continue
}
r := detectors.Result{
DetectorType: detectorspb.DetectorType_AlgoliaAdminKey,
Raw: []byte(key),
RawV2: []byte(id + ":" + key),
}
if verify {
// Verify if the key is a valid Algolia Admin Key.
isVerified, extraData, verificationErr := verifyMatch(ctx, id, key)
r.Verified = isVerified
r.ExtraData = extraData
if verificationErr != nil {
if errors.Is(verificationErr, errNoHost) {
invalidHosts.Set(id, struct{}{})
continue
}
r.SetVerificationError(verificationErr, key)
}
}
results = append(results, r)
if r.Verified {
break
}
}
}
return results, nil
}
// https://www.algolia.com/doc/guides/security/api-keys/#access-control-list-acl
var nonSensitivePermissions = map[string]struct{}{
"listIndexes": {},
"search": {},
"settings": {},
}
func verifyMatch(ctx context.Context, appId, apiKey string) (bool, map[string]string, error) {
// https://www.algolia.com/doc/rest-api/search/#section/Base-URLs
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://"+appId+".algolia.net/1/keys/"+apiKey, nil)
if err != nil {
return false, nil, err
}
req.Header.Set("X-Algolia-Application-Id", appId)
req.Header.Set("X-Algolia-API-Key", apiKey)
res, err := client.Do(req)
if err != nil {
// lookup xyz.algolia.net: no such host
if strings.Contains(err.Error(), "no such host") {
return false, nil, errNoHost
}
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
var keyRes keyResponse
if err := json.NewDecoder(res.Body).Decode(&keyRes); err != nil {
return false, nil, err
}
// Check if the key has sensitive permissions, even if it's not an Admin Key.
hasSensitivePerms := false
for _, acl := range keyRes.ACL {
if _, ok := nonSensitivePermissions[acl]; !ok {
hasSensitivePerms = true
break
}
}
if !hasSensitivePerms {
return false, nil, nil
}
slices.Sort(keyRes.ACL)
extraData := map[string]string{
"acl": strings.Join(keyRes.ACL, ","),
}
if keyRes.Description != "" && keyRes.Description != "" {
extraData["description"] = keyRes.Description
}
return true, extraData, nil
case http.StatusUnauthorized:
return false, nil, nil
case http.StatusForbidden:
// Invalidated key.
// {"message":"Invalid Application-ID or API key","status":403}
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
// https://www.algolia.com/doc/rest-api/search/#tag/Api-Keys/operation/getApiKey
type keyResponse struct {
ACL []string `json:"acl"`
Description string `json:"description"`
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AlgoliaAdminKey
}
func (s Scanner) Description() string {
return "Algolia is a search-as-a-service platform. Algolia Admin Keys can be used to manage indices and API keys, and perform administrative tasks."
}
================================================
FILE: pkg/detectors/algoliaadminkey/algoliaadminkey_integration_test.go
================================================
//go:build detectors
// +build detectors
package algoliaadminkey
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAlgoliaAdminKey_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ALGOLIAADMINKEY_TOKEN")
inactiveSecret := testSecrets.MustGetField("ALGOLIAADMINKEY_INACTIVE")
id := testSecrets.MustGetField("ALGOLIAADMINKEY_APPID")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a algolia secret %s within algolia %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AlgoliaAdminKey,
Verified: true,
RawV2: []byte(fmt.Sprintf("%s%s", secret, id)),
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a algolia secret %s within algolia %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AlgoliaAdminKey,
Verified: false,
RawV2: []byte(fmt.Sprintf("%s%s", inactiveSecret, id)),
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AlgoliaAdminKey.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("AlgoliaAdminKey.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/algoliaadminkey/algoliaadminkey_test.go
================================================
package algoliaadminkey
import (
"context"
"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"
)
func TestAlgoliaAdminKey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using algolia Key=BsDaN7ZU7kFiUX5CpN8CUf3nkMaSeZYn
[DEBUG] Using docsearch ID=844XQV5SUA
[INFO] Response received: 200 OK
`,
want: []string{"844XQV5SUA:BsDaN7ZU7kFiUX5CpN8CUf3nkMaSeZYn"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{appId 0VJ9I1WV78}{algolia AQAAABAAA 4AYm3wz7nfnX7Bqtw5e5Qo3Z5vfBe0eS}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"0VJ9I1WV78:4AYm3wz7nfnX7Bqtw5e5Qo3Z5vfBe0eS"},
},
{
name: "valid pattern - key out of prefix range",
input: `
[INFO] Sending request to the algolia API
[DEBUG] Using Key=BsDaN7ZU7kFiUX5CpN8CUf3nkMaSeZYn
[DEBUG] Using ID=844XQV5SUA
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using algolia Key=BsD-N7ZU7kFiUX5CpN8CUf3nkMaSeZYn
[DEBUG] Using docsearch ID=844XqV5SUA
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/alibaba/alibaba.go
================================================
package alibaba
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
client *http.Client
}
type alibabaResp struct {
RequestId string `json:"RequestId"`
Message string `json:"Message"`
Recommend string `json:"Recommend"`
HostId string `json:"HostId"`
Code string `json:"Code"`
}
const alibabaURL = "https://ecs.aliyuncs.com"
var (
// Ensure the Scanner satisfies the interface at compile time.
_ detectors.Detector = (*Scanner)(nil)
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b([a-zA-Z0-9]{30})\b`)
idPat = regexp.MustCompile(`\b(LTAI[a-zA-Z0-9]{17,21})[\"';\s]*`)
)
// 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{"LTAI"}
}
func (s Scanner) Description() string {
return "Alibaba Cloud is a cloud computing service that provides a suite of cloud computing services including data storage, relational databases, big-data processing, and content delivery networks (CDNs). Alibaba Cloud API keys can be used to access and manage these services."
}
func randString(n int) string {
const alphanum = "0123456789abcdefghijklmnopqrstuvwxyz"
var bytes = make([]byte, n)
_, _ = rand.Read(bytes)
for i, b := range bytes {
bytes[i] = alphanum[b%byte(len(alphanum))]
}
return string(bytes)
}
func GetSignature(input, key string) string {
key_for_sign := []byte(key)
h := hmac.New(sha1.New, key_for_sign)
h.Write([]byte(input))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func buildStringToSign(method, input string) string {
filter := strings.Replace(input, "+", "%20", -1)
filter = strings.Replace(filter, "%7E", "~", -1)
filter = strings.Replace(filter, "*", "%2A", -1)
filter = method + "&%2F&" + url.QueryEscape(filter)
return filter
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Alibaba 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
resIdMatch := strings.TrimSpace(idMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Alibaba,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch),
}
if verify {
client := s.getClient()
isVerified, verificationErr := verifyAlibaba(ctx, client, resIdMatch, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyAlibaba(ctx context.Context, client *http.Client, resIdMatch, resMatch string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, alibabaURL, nil)
if err != nil {
return false, err
}
dateISO := time.Now().UTC().Format("2006-01-02T15:04:05Z07:00")
params := req.URL.Query()
params.Add("AccessKeyId", resIdMatch)
params.Add("Action", "DescribeRegions")
params.Add("Format", "JSON")
params.Add("SignatureMethod", "HMAC-SHA1")
params.Add("SignatureNonce", randString(16))
params.Add("SignatureVersion", "1.0")
params.Add("Timestamp", dateISO)
params.Add("Version", "2014-05-26")
stringToSign := buildStringToSign(req.Method, params.Encode())
signature := GetSignature(stringToSign, resMatch+"&") // Get Signature HMAC SHA1
params.Add("Signature", signature)
req.URL.RawQuery = params.Encode()
req.Header.Add("Content-Type", "text/xml;charset=utf-8")
req.Header.Add("Content-Length", strconv.Itoa(len(params.Encode())))
res, err := client.Do(req)
if err != nil {
return false, err
}
defer res.Body.Close()
var alibabaResp alibabaResp
if err = json.NewDecoder(res.Body).Decode(&alibabaResp); err != nil {
return false, err
}
switch res.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusNotFound, http.StatusBadRequest:
// 400 used for most of error cases
// 404 used if the AccessKeyId is not valid
return false, nil
default:
err := fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
if alibabaResp.Message != "" {
err = fmt.Errorf("%s: %s, %s", err, alibabaResp.Message, alibabaResp.Code)
}
return false, err
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Alibaba
}
================================================
FILE: pkg/detectors/alibaba/alibaba_integration_test.go
================================================
//go:build detectors
// +build detectors
package alibaba
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAlibaba_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ALIBABA_SECRET")
inactiveSecret := testSecrets.MustGetField("ALIBABA_SECRET_INACTIVE")
id := testSecrets.MustGetField("ALIBABA_ID")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a alibaba secret %s within alibaba %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Alibaba,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, real secrets, verification error due to timeout",
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a alibaba secret %s within alibaba %s", secret, id)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Alibaba,
Verified: false,
}
r.SetVerificationError(context.DeadlineExceeded)
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, real secrets, verification error due to unexpected api surface",
s: Scanner{client: common.ConstantResponseHttpClient(500, "{}")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a alibaba secret %s within alibaba %s", secret, id)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Alibaba,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 500"))
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, real secrets, verification error due to broken json",
s: Scanner{client: common.ConstantResponseHttpClient(418, "{")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a alibaba secret %s within alibaba %s", secret, id)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Alibaba,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected EOF"))
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a alibaba secret %s within alibaba %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Alibaba,
Verified: false,
},
},
wantErr: 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,
},
}
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("Alibaba.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")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Alibaba.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/alibaba/alibaba_test.go
================================================
package alibaba
import (
"context"
"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"
)
func TestAliBaba_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using Key=CwgR2UwgaWd7hgUdQkwFnK9vvEeO4R
[DEBUG] Using ID=LTAIXgRPqwF1DhBf6Q1uZ5DrM
[INFO] Response received: 200 OK
`,
want: []string{"CwgR2UwgaWd7hgUdQkwFnK9vvEeO4RLTAIXgRPqwF1DhBf6Q1uZ5DrM"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{WX6OtM8pbcrXWMIGc5evYousFWBlBm}{AQAAABAAA LTAImg3ZeAPatbAtEDS9HVZ}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"WX6OtM8pbcrXWMIGc5evYousFWBlBmLTAImg3ZeAPatbAtEDS9HVZ"},
},
{
name: "valid pattern - ignore special characters at end",
input: `
[INFO] Sending request to the API
[DEBUG] Using Key=CwgR2UwgaWd7hgUdQkwFnK9vvEeO4R
[DEBUG] Using ID=LTAIXgRPqwF1DhBf6Q1uZ5DrM;
[INFO] Response received: 200 OK
`,
want: []string{"CwgR2UwgaWd7hgUdQkwFnK9vvEeO4RLTAIXgRPqwF1DhBf6Q1uZ5DrM"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using Key=CwgR2UwgaWd7hgUdQkwFnK9vvEeO4
[DEBUG] Using ID=LTAIXgRPqwF1DhBf6Q1uZ5DrMYPW
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/alienvault/alienvault.go
================================================
package alienvault
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"alienvault"}) + `\b([a-z0-9]{64})\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{"alienvault"}
}
// FromData will find and optionally verify AlienVault 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AlienVault,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAlienVaultKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AlienVault
}
func verifyAlienVaultKey(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://otx.alienvault.com/api/v1/users/me", nil)
if err != nil {
return false, err
}
req.Header.Add("X-OTX-API-KEY", key)
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Description() string {
return "AlienVault is a threat intelligence platform providing real-time data on emerging threats. AlienVault API keys can be used to access threat data and other services."
}
================================================
FILE: pkg/detectors/alienvault/alienvault_integration_test.go
================================================
//go:build detectors
// +build detectors
package alienvault
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAlienVault_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ALIENVAULT")
inactiveSecret := testSecrets.MustGetField("ALIENVAULT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a alienvault secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AlienVault,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a alienvault secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AlienVault,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AlienVault.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("AlienVault.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/alienvault/alienvault_test.go
================================================
package alienvault
import (
"context"
"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"
)
func TestAlienVault_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the alienvault API
[DEBUG] Using Key=3em7p52ec9ut4k9ccqha19rz3oyeqnij3mn3ivml577f8pb2179yz9totr648hmy
[INFO] Response received: 200 OK
`,
want: []string{"3em7p52ec9ut4k9ccqha19rz3oyeqnij3mn3ivml577f8pb2179yz9totr648hmy"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{alienvault}{AQAAABAAA xyi7bj56t5b0hkinw4vz8qgffqhfb2ypemdnt407bke6s0ouuswvcdf5c1qpvse0}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"xyi7bj56t5b0hkinw4vz8qgffqhfb2ypemdnt407bke6s0ouuswvcdf5c1qpvse0"},
},
{
name: "valid pattern - key out of prefix range",
input: `
[INFO] Fetching data from alienvault
[INFO] Sending request to the API
[DEBUG] Using Key=3em7p52ec9ut4k9ccqha19rz3oyeqnij3mn3ivml577f8pb2179yz9totr648hmy
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the alienvault API
[DEBUG] Using Key=3em7p52ec9ut4k9ccqha19rz3o_eqnij3mn3ivml577f8pb2179yz9totr648hmy
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/allsports/allsports.go
================================================
package allsports
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"allsports"}) + `\b([0-9a-z]{64})\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{"allsports"}
}
// FromData will find and optionally verify Allsports 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Allsports,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAllSportsKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyAllSportsKey(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://apiv2.allsportsapi.com/football/?met=Countries&APIkey="+key, nil)
if err != nil {
return false, err
}
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:
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
body := string(bodyBytes)
if strings.Contains(body, "Wrong login credentials") {
return false, nil
}
return true, nil
case http.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Allsports
}
func (s Scanner) Description() string {
return "Allsports API keys can be used to access and interact with the Allsports API, allowing retrieval of sports data and other related operations."
}
================================================
FILE: pkg/detectors/allsports/allsports_integration_test.go
================================================
//go:build detectors
// +build detectors
package allsports
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAllsports_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ALLSPORTS")
inactiveSecret := testSecrets.MustGetField("ALLSPORTS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a allsports secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Allsports,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a allsports secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Allsports,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Allsports.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Allsports.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/allsports/allsports_test.go
================================================
package allsports
import (
"context"
"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"
)
func TestAllSports_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the allsports API
[DEBUG] Using Key=cq73u5azj3p3shfvzz3lw1typfqu6uduq7bophtq4veta7cnvd4s5htkb8lgk4vr
[INFO] Response received: 200 OK
`,
want: []string{"cq73u5azj3p3shfvzz3lw1typfqu6uduq7bophtq4veta7cnvd4s5htkb8lgk4vr"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{allsports}{AQAAABAAA bj8yzu3awie5akwiwcb7esqygqx14gt65j9lrcpec0v28ckkswtyza1x9747gap5}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"bj8yzu3awie5akwiwcb7esqygqx14gt65j9lrcpec0v28ckkswtyza1x9747gap5"},
},
{
name: "valid pattern - key out of prefix range",
input: `
[DEBUG] allsports api processing
[INFO] Sending request to the API
[DEBUG] Using Key=cq73u5azj3p3shfvzz3lw1typfqu6uduq7bophtq4veta7cnvd4s5htkb8lgk4vr
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the allsports API
[DEBUG] Using Key=d1f2e3c4b5a6d7e8f9G0h1i2j3k4l5m6n7o8p9q0r1s2t3u4v5w6x7y8z9a0b1ce
[INFO] Response received: 200 OK
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/amadeus/amadeus.go
================================================
package amadeus
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"amadeus"}) + `\b([0-9A-Za-z]{32})\b`)
secretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"amadeus"}) + `\b([0-9A-Za-z]{16})\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{"amadeus"}
}
// FromData will find and optionally verify Amadeus 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)
var uniqueKeys, uniqueSecrets = make(map[string]struct{}), make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for _, matches := range secretPat.FindAllStringSubmatch(dataStr, -1) {
uniqueSecrets[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
for secret := range uniqueSecrets {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Amadeus,
Raw: []byte(key),
RawV2: []byte(key + secret),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAdobeIOSecret(ctx, client, key, secret)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyAdobeIOSecret(ctx context.Context, client *http.Client, key string, secret string) (bool, error) {
payload := strings.NewReader("grant_type=client_credentials&client_id=" + key + "&client_secret=" + secret)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://test.api.amadeus.com/v1/security/oauth2/token", payload)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
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:
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
body := string(bodyBytes)
if !strings.Contains(body, "access_token") {
return false, nil
}
return true, nil
case http.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Amadeus
}
func (s Scanner) Description() string {
return "Amadeus provides travel technology solutions. Amadeus API keys can be used to access and modify travel-related data and services."
}
================================================
FILE: pkg/detectors/amadeus/amadeus_integration_test.go
================================================
//go:build detectors
// +build detectors
package amadeus
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAmadeus_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
id := testSecrets.MustGetField("AMADEUS")
secret := testSecrets.MustGetField("AMADEUS_SECRET")
inactiveSecret := testSecrets.MustGetField("AMADEUS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a amadeus secret %s within amadeus id %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Amadeus,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a amadeus secret %s within amadeus id %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Amadeus,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Amadeus.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Amadeus.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/amadeus/amadeus_test.go
================================================
package amadeus
import (
"context"
"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"
)
func TestAmadeus_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using amadeus Key=ttdveNai3Gj6Zrjvgz4fyBEWRLARCG6a
[DEBUG] Using amadeus Secret=9wqrSr2qveaqgQns
[INFO] Response received: 200 OK
`,
want: []string{"ttdveNai3Gj6Zrjvgz4fyBEWRLARCG6a9wqrSr2qveaqgQns"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{amadeus ey6U46qCx26dqzMVWAGiibt6m65mM5w9}{amadeus AQAAABAAA Ew3TfmLHYaRjPnYO}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"ey6U46qCx26dqzMVWAGiibt6m65mM5w9Ew3TfmLHYaRjPnYO"},
},
{
name: "valid pattern - key out of prefix range",
input: `
[INFO] Sending request to the amadeus API
[DEBUG] Using Key=ttdveNai3Gj6Zrjvgz4fyBEWRLARCG6a
[DEBUG] Using Secret=9wqrSr2qveaqgQns
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the amadeus API
[DEBUG] Using amadeus Key=tthdveNai3Gj6Zrjvgz4fyBEWRLARCG6a
[DEBUG] Using amadeus Secret=9wqrSr2qveacqgQns
[INFO] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/ambee/ambee.go
================================================
package ambee
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ambee"}) + `\b([0-9a-f]{64})\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{"ambee"}
}
// FromData will find and optionally verify Ambee 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Ambee,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAmbeeKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyAmbeeKey(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.ambeedata.com/latest/by-lat-lng?lat=12&lng=77", nil)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("x-api-key", key)
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Ambee
}
func (s Scanner) Description() string {
return "Ambee provides environmental and climate data APIs. Ambee API keys can be used to access this data for various applications such as weather forecasting, air quality monitoring, and more."
}
================================================
FILE: pkg/detectors/ambee/ambee_integration_test.go
================================================
//go:build detectors
// +build detectors
package ambee
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAmbee_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AMBEE")
inactiveSecret := testSecrets.MustGetField("AMBEE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a ambee secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Ambee,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a ambee secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Ambee,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Ambee.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Ambee.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/ambee/ambee_test.go
================================================
package ambee
import (
"context"
"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"
)
func TestAmbee_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the ambee API
[DEBUG] Using Key=eccb41cc2d4dab96b748ed040e9b308161279820447ef4553ba6e6d20ecb9962
[INFO] Response received: 200 OK
`,
want: []string{"eccb41cc2d4dab96b748ed040e9b308161279820447ef4553ba6e6d20ecb9962"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{ambee}{ambee AQAAABAAA b91280c63e1571ad928d52947cc31a14ad1bf5a83088d0346b94f6683cf22138}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"b91280c63e1571ad928d52947cc31a14ad1bf5a83088d0346b94f6683cf22138"},
},
{
name: "valid pattern - key out of prefix range",
input: `
[INFO] Fetching data from ambee
[INFO] Sending request to the API
[DEBUG] Using Key=eccb41cc2d4dab96b748ed040e9b308161279820447ef4553ba6e6d20ecb9962
[INFO] Response received: 200 OK
`,
want: nil,
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the ambee API
[DEBUG] Using Key=eccb41cc2d4dab96y748ed040e9b308161279820447ef4553ba6e6d20ecb9962
[INFO] Response received: 200 OK
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/amplitudeapikey/amplitudeapikey.go
================================================
package amplitudeapikey
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"amplitude"}) + `\b([0-9a-f]{32})\b`)
secretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"amplitude"}) + `\b([0-9a-f]{32})\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{"amplitude"}
}
// FromData will find and optionally verify AmplitudeApiKey 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)
var uniqueKeys, uniqueSecrets = make(map[string]struct{}), make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for _, matches := range secretPat.FindAllStringSubmatch(dataStr, -1) {
uniqueSecrets[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
for secret := range uniqueSecrets {
// regex for both key and secret are same so the set of strings could possibly be same as well
if key == secret {
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AmplitudeApiKey,
Raw: []byte(key),
RawV2: []byte(key + secret),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAdobeIOSecret(ctx, client, key, secret)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyAdobeIOSecret(ctx context.Context, client *http.Client, key string, secret string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://amplitude.com/api/2/taxonomy/category", nil)
if err != nil {
return false, err
}
req.SetBasicAuth(key, secret)
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AmplitudeApiKey
}
func (s Scanner) Description() string {
return "Amplitude is a product analytics service that helps companies track and analyze user behavior within web and mobile applications. Amplitude API keys can be used to access and modify this data."
}
================================================
FILE: pkg/detectors/amplitudeapikey/amplitudeapikey_integration_test.go
================================================
//go:build detectors
// +build detectors
package amplitudeapikey
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAmplitudeApiKey_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
key := testSecrets.MustGetField("AMPLITUDEAPI_KEY")
secret := testSecrets.MustGetField("AMPLITUDEAPI_SECRET")
inactiveKey := testSecrets.MustGetField("AMPLITUDEAPI_KEY_INACTIVE")
inactiveSecret := testSecrets.MustGetField("AMPLITUDEAPI_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find an amplitude key %s with amplitude secret %s within", key, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AmplitudeApiKey,
Verified: true,
},
{
DetectorType: detectorspb.DetectorType_AmplitudeApiKey,
Verified: false,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find an amplitude key %s with amplitude secret %s within but not valid", inactiveKey, inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AmplitudeApiKey,
Verified: false,
},
{
DetectorType: detectorspb.DetectorType_AmplitudeApiKey,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AmplitudeApiKey.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])
}
got[i].Raw = nil
if len(got[i].RawV2) == 0 {
t.Fatalf("no rawv2 secret present: \n %+v", got[i])
}
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("AmplitudeApiKey.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/amplitudeapikey/amplitudeapikey_test.go
================================================
package amplitudeapikey
import (
"context"
"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"
)
func TestAmplitudeAPIKey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using amplitude Key=c2167730016af34b89e200ecf55710e8
[DEBUG] Using amplitude Secret=5488620aa9073c09f1a16e2b1dc357b6
[INFO] Response received: 200 OK
`,
want: []string{
"c2167730016af34b89e200ecf55710e85488620aa9073c09f1a16e2b1dc357b6",
"5488620aa9073c09f1a16e2b1dc357b6c2167730016af34b89e200ecf55710e8",
},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{amplitude aac639f65d80ec2eec96e775f598ce13}{amplitude AQAAABAAA 8ac62041353622f9c5e4657807ff1eac}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{
"aac639f65d80ec2eec96e775f598ce138ac62041353622f9c5e4657807ff1eac",
"8ac62041353622f9c5e4657807ff1eacaac639f65d80ec2eec96e775f598ce13",
},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using amplitude Key=c2167730016rf34b89e200ecf55710e8
[DEBUG] Using amplitude Secret=5488620aa9073q09f1a16e2b1dc357b6
[INFO] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/anthropic/anthropic.go
================================================
package anthropic
import (
"context"
"errors"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(sk-ant-(?:admin01|api03)-[\w\-]{93}AA)\b`)
// verification endpoints
apiKeyEndpoint = "https://api.anthropic.com/v1/models"
adminKeyEndpoint = "https://api.anthropic.com/v1/organizations/api_keys"
)
// 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{"sk-ant-api03", "sk-ant-admin01"}
}
// FromData will find and optionally verify Anthropic 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)
keys := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, key := range keys {
keyMatch := strings.TrimSpace(key[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Anthropic,
Raw: []byte(keyMatch),
ExtraData: make(map[string]string),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isAdminKey := isAdminKey(keyMatch)
var isVerified bool
var err error
if isAdminKey {
isVerified, err = verifyAnthropicKey(ctx, client, adminKeyEndpoint, keyMatch)
s1.ExtraData["Type"] = "Admin Key"
} else if !isAdminKey {
isVerified, err = verifyAnthropicKey(ctx, client, apiKeyEndpoint, keyMatch)
s1.ExtraData["Type"] = "API Key"
} else {
return nil, errors.New("unknown key type detected for anthropic")
}
s1.Verified = isVerified
s1.SetVerificationError(err, keyMatch)
if s1.Verified {
s1.AnalysisInfo = map[string]string{
"key": keyMatch,
}
}
}
results = append(results, s1)
}
return results, nil
}
/*
verifyAnthropicKey verify the anthropic key passed against the endpoint
Endpoints:
- For api keys: https://docs.anthropic.com/en/api/models-list
- For admin keys: https://docs.anthropic.com/en/api/admin-api/apikeys/list-api-keys
*/
func verifyAnthropicKey(ctx context.Context, client *http.Client, endpoint, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody)
if err != nil {
return false, nil
}
req.Header.Set("x-api-key", key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("anthropic-version", "2023-06-01")
res, err := client.Do(req)
if err != nil {
return false, err
}
defer res.Body.Close()
switch res.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusNotFound, http.StatusUnauthorized:
// 404 is returned if api key is disabled or not found
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Anthropic
}
func (s Scanner) Description() string {
return "Anthropic is an AI research company. The API keys can be used to access their AI models and services."
}
func isAdminKey(key string) bool {
return strings.HasPrefix(key, "sk-ant-admin01")
}
================================================
FILE: pkg/detectors/anthropic/anthropic_integration_test.go
================================================
//go:build detectors
// +build detectors
package anthropic
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAnthropic_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)
}
apiKey := testSecrets.MustGetField("ANTHROPIC")
inactiveSecret := testSecrets.MustGetField("ANTHROPIC_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a anthropic secret %s within", apiKey)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Anthropic,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a anthropic secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Anthropic,
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 anthropic secret %s within", apiKey)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Anthropic,
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("Anthropic.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "AnalysisInfo")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Anthropic.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/anthropic/anthropic_test.go
================================================
package anthropic
import (
"context"
"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"
)
func TestAnthropic_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
System Log - Authentication Token Issued
Date: 2025-02-04 14:32:10 UTC
Server: api-secure-03.internal
Service: Anthropic API Gateway
API Key: sk-ant-api03-abc123xyz-456def789ghij-klmnopqrstuvwx-3456yza789bcde-1234fghijklmnopby56aaaogaopaaaabc123xyzAA
Admin Key: sk-ant-admin01-abc12fake-456def789ghij-klmnopqrstuvwx-3456yza789bcde-12fakehijklmnopby56aaaogaopaaaabc123xyzAA
Log Entry:
A new API and Admin key has been generated for service authentication. Please ensure that this key remains confidential and is not exposed in any public repositories or logs.
`,
want: []string{
"sk-ant-api03-abc123xyz-456def789ghij-klmnopqrstuvwx-3456yza789bcde-1234fghijklmnopby56aaaogaopaaaabc123xyzAA",
"sk-ant-admin01-abc12fake-456def789ghij-klmnopqrstuvwx-3456yza789bcde-12fakehijklmnopby56aaaogaopaaaabc123xyzAA",
},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{anthropic}{AQAAABAAA sk-ant-api03-Dtjm9IZ_rYhS_ihHLZmPXhjJ6PN8UPp7vNO7qO3735RRDpf8xbWGinsch0McONXznUm-4KWoA7WU2otvvwHBR5QRjiLakAA}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"sk-ant-api03-Dtjm9IZ_rYhS_ihHLZmPXhjJ6PN8UPp7vNO7qO3735RRDpf8xbWGinsch0McONXznUm-4KWoA7WU2otvvwHBR5QRjiLakAA"},
},
{
name: "invalid pattern",
input: `
System Log - Authentication Token Issued
Date: 2025-02-04 14:32:10 UTC
Server: api-secure-03.internal
Service: Anthropic API Gateway
API Key: sk-ant-api03-abc123xyz-456de-klMnopqrstuvwx-3456yza789bcde-1234fghijklmnopAA
Log Entry:
A new API key has been generated for service authentication. Please ensure that this key remains confidential and is not exposed in any public repositories or logs.
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/anypoint/anypoint.go
================================================
package anypoint
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})\b`)
orgPat = regexp.MustCompile(detectors.PrefixRegex([]string{"org"}) + `\b([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})\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{"anypoint"}
}
// FromData will find and optionally verify Anypoint 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)
var uniqueKeys, uniquePats = make(map[string]struct{}), make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for _, matches := range orgPat.FindAllStringSubmatch(dataStr, -1) {
uniquePats[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
for org := range uniquePats {
// regex for both key and org are same, so to avoid same string processing
if key == org {
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Anypoint,
Raw: []byte(key),
RawV2: []byte(key + org),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAnypointSecret(ctx, client, key, org)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyAnypointSecret(ctx context.Context, client *http.Client, key string, org string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://anypoint.mulesoft.com/apiplatform/repository/v2/organizations/%s/apis/by-name?apiName=%s", org, ""), nil)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Anypoint
}
func (s Scanner) Description() string {
return "Anypoint is a unified platform that allows organizations to build and manage APIs and integrations. Anypoint credentials can be used to access and manipulate these integrations and API data."
}
================================================
FILE: pkg/detectors/anypoint/anypoint_integration_test.go
================================================
//go:build detectors
// +build detectors
package anypoint
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAnypoint_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ANYPOINT_TOKEN")
inactiveSecret := testSecrets.MustGetField("ANYPOINT_INACTIVE")
organizationId := testSecrets.MustGetField("ANYPOINT_ORGANIZATIONID")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find an anypoint secret %s within organization %s", secret, organizationId)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Anypoint,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find an anypoint secret %s within organization %s but not valid", inactiveSecret, organizationId)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Anypoint,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Anypoint.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Anypoint.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/anypoint/anypoint_test.go
================================================
package anypoint
import (
"context"
"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"
)
func TestAnypoint_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
# Anypoint Secret Configuration File
# Organization details
ORG_NAME=my_organization
ORG_ID=abcd1234-ef56-gh78-ij90-klmn1234opqr
# OAuth tokens
ACCESS_TOKEN=abcxyz123
REFRESH_TOKEN=zyxwvutsrqponmlkji9876543210abcd
# API keys
SECRET_KEY=1a2b3c4d-5e6f-7g8h-9i0j-k1l2m3n4o5p6
# Endpoints
SERVICE_URL=https://api.example.com/v1/resource
`,
want: []string{"1a2b3c4d-5e6f-7g8h-9i0j-k1l2m3n4o5p6abcd1234-ef56-gh78-ij90-klmn1234opqr"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{anypoint org rdogw4dd-6x3l-2nm3-jvl5-qi8dyheccgj7}{AQAAABAAA 7jhlugw8-3tfb-7ju2-0i0y-7un6qxvknbvz}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"7jhlugw8-3tfb-7ju2-0i0y-7un6qxvknbvzrdogw4dd-6x3l-2nm3-jvl5-qi8dyheccgj7"},
},
{
name: "invalid pattern",
input: `
# Anypoint Secret Configuration File
# Organization details
ORG_NAME=my_organization
ORG_ID=abcd1234-ef56-gh78-ij90-klmn1234opqr
# OAuth tokens
ACCESS_TOKEN=abcxyz123
REFRESH_TOKEN=zyxwvutsrqponmlkji9876543210abcd
# API keys
SECRET_KEY=1a2b3C4d-5E6f-7g8H-9i0J-k1l2M3n4o5p6
# Endpoints
SERVICE_URL=https://api.example.com/v1/resource
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/anypointoauth2/anypointoauth2.go
================================================
package anypointoauth2
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"anypoint", "id"}) + `\b([0-9a-f]{32})\b`)
secretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"anypoint", "secret"}) + `\b([0-9a-fA-F]{32})\b`)
verificationUrl = "https://anypoint.mulesoft.com/accounts/oauth2/token"
)
// 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{"anypoint"}
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Anypoint 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)
var uniqueIDs, uniqueSecrets = make(map[string]struct{}), make(map[string]struct{})
for _, matches := range idPat.FindAllStringSubmatch(dataStr, -1) {
uniqueIDs[matches[1]] = struct{}{}
}
for _, matches := range secretPat.FindAllStringSubmatch(dataStr, -1) {
uniqueSecrets[matches[1]] = struct{}{}
}
for id := range uniqueIDs {
for secret := range uniqueSecrets {
if id == secret {
// Avoid processing the same string for both id and secret.
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AnypointOAuth2,
Raw: []byte(secret),
RawV2: []byte(fmt.Sprintf("%s:%s", id, secret)),
}
if verify {
client := s.getClient()
isVerified, verificationErr := verifyMatch(ctx, client, id, secret)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
if isVerified {
s1.AnalysisInfo = map[string]string{
"id": id,
"secret": secret,
}
}
}
results = append(results, s1)
if s1.Verified {
// Anypoint client IDs and secrets are mapped one-to-one, so if a pair
// is verified, we can remove that secret from the uniqueSecrets map.
delete(uniqueSecrets, secret)
break
}
}
}
return
}
func verifyMatch(ctx context.Context, client *http.Client, id, secret string) (bool, error) {
payload := strings.NewReader(`{"grant_type":"client_credentials","client_id":"` + id + `","client_secret":"` + secret + `"}`)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, verificationUrl, payload)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
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 {
// The endpoint responds with status 200 for valid Organization credentials and 422 for Client credentials.
case http.StatusOK, http.StatusUnprocessableEntity:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AnypointOAuth2
}
func (s Scanner) Description() string {
return "Anypoint is a unified platform that allows organizations to build and manage APIs and integrations. Anypoint credentials can be used to access and manipulate these integrations and API data."
}
================================================
FILE: pkg/detectors/anypointoauth2/anypointoauth2_integration_test.go
================================================
//go:build detectors
// +build detectors
package anypointoauth2
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAnypointOAuth2_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
clientID := testSecrets.MustGetField("ANYPOINT_CLIENT_ID")
clientSecret := testSecrets.MustGetField("ANYPOINT_CLIENT_SECRET")
inactiveSecret := testSecrets.MustGetField("ANYPOINT_INACTIVE_SECRET")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find an anypoint secret %s within anypoint organization id %s", clientSecret, clientID)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AnypointOAuth2,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find an anypoint secret %s within anypoint organization id %s but not valid", inactiveSecret, clientID)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AnypointOAuth2,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Anypoint.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", "primarySecret")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AnypointOAuth2.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/anypointoauth2/anypointoauth2_test.go
================================================
package anypointoauth2
import (
"context"
"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"
)
func TestAnypoint_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
# Anypoint Secret Configuration File
# Organization details
ORG_NAME=my_organization
ORG_ID=abcd1234-ef56-gh78-ij90-klmn1234opqr
# OAuth tokens
CLIENT_ID=e3cd10a87f53b2dfa4b5fd606e7d9eca
CLIENT_SECRET=ACE9d7E606Df5B4AFD2B35f78A01DC3E
# API keys
SECRET_KEY=1a2b3c4d-5e6f-7g8h-9i0j-k1l2m3n4o5p6
# Endpoints
SERVICE_URL=https://api.example.com/v1/resource
`,
want: []string{"e3cd10a87f53b2dfa4b5fd606e7d9eca:ACE9d7E606Df5B4AFD2B35f78A01DC3E"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{anypoint id 17c55fba5c93de5646b10507c36fbc23}{AQAAABAAA 8E6Ef8F8d5De05d8BF1491e1ecC37b31}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"17c55fba5c93de5646b10507c36fbc23:8E6Ef8F8d5De05d8BF1491e1ecC37b31"},
},
{
name: "invalid pattern",
input: `
# Anypoint Secret Configuration File
# Organization details
ORG_NAME=my_organization
ORG_ID=abcd1234-ef56-gh78-ij90-klmn1234opqr
# OAuth tokens
CLIENT_ID=k4lzc5ty98tnfu3a11y8gnv5vb1281as
CLIENT_SECRET=ACE9d7E606Df5B4AFD2B35f78A01DC3E
# API keys
SECRET_KEY=1a2b3c4d-5e6f-7g8h-9i0j-k1l2m3n4o5p6
# Endpoints
SERVICE_URL=https://api.example.com/v1/resource
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/apacta/apacta.go
================================================
package apacta
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"apacta"}) + `\b([a-z0-9-]{36})\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{"apacta"}
}
// FromData will find and optionally verify Apacta 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Apacta,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyApactaKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyApactaKey(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://app.apacta.com/api/v1/time_entries?api_key=%s", key), nil)
if err != nil {
return false, err
}
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Apacta
}
func (s Scanner) Description() string {
return "Apacta is a project management tool designed for the construction industry. Apacta API keys can be used to access and manage project data within the Apacta platform."
}
================================================
FILE: pkg/detectors/apacta/apacta_integration_test.go
================================================
//go:build detectors
// +build detectors
package apacta
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestApacta_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("APACTA")
inactiveSecret := testSecrets.MustGetField("APACTA_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apacta secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Apacta,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apacta secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Apacta,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Apacta.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Apacta.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/apacta/apacta_test.go
================================================
package apacta
import (
"context"
"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"
)
func TestApacta_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
// Create a new request with the secret as a header
req, err := http.NewRequest("POST", "https://api.example.com/v1/resource", bytes.NewBuffer([]byte("{}")))
if err != nil {
fmt.Println("Error creating request:", err)
return
}
apactaSecret := "Bearer abcd1234-ef56-gh78-ij90-klmn1234opqr"
req.Header.Set("Authorization", apactaSecret)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"abcd1234-ef56-gh78-ij90-klmn1234opqr"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{apacta}{AQAAABAAA w8-p59rc70q0unyupknadu5sr8bf5us04mpt}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"w8-p59rc70q0unyupknadu5sr8bf5us04mpt"},
},
{
name: "invalid pattern",
input: `
func main() {
// Create a new request with the secret as a header
req, err := http.NewRequest("POST", "https://api.example.com/v1/resource", bytes.NewBuffer([]byte("{}")))
if err != nil {
fmt.Println("Error creating request:", err)
return
}
apactaSecret := "Bearer abcD$1234-ef56-gH78-ij90-klmn1234opqr"
req.Header.Set("Authorization", apactaSecret)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/api2cart/api2cart.go
================================================
package api2cart
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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"api2cart"}) + `\b([0-9a-f]{32})\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{"api2cart"}
}
// FromData will find and optionally verify Api2Cart 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Api2Cart,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyApi2CartKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyApi2CartKey(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://api.api2cart.com/v1.1/account.cart.list.json?api_key=%s", key), nil)
if err != nil {
return false, err
}
req.Header.Add("Accept", "application/json")
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:
body, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
var result Response
if err := json.Unmarshal(body, &result); err != nil {
return false, err
}
if result.ReturnCode == 0 {
return true, nil
}
case http.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
return false, nil
}
type Response struct {
ReturnCode int `json:"return_code"`
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Api2Cart
}
func (s Scanner) Description() string {
return "Api2Cart is a unified shopping cart data interface that allows interaction with multiple shopping cart platforms. Api2Cart API keys can be used to access and manipulate shopping cart data."
}
================================================
FILE: pkg/detectors/api2cart/api2cart_integration_test.go
================================================
//go:build detectors
// +build detectors
package api2cart
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestApi2Cart_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("API2CART")
inactiveSecret := testSecrets.MustGetField("API2CART_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a api2cart secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Api2Cart,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a api2cart secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Api2Cart,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Api2Cart.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Api2Cart.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/api2cart/api2cart_test.go
================================================
package api2cart
import (
"context"
"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"
)
func TestApi2Cart_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
To integrate with API2Cart, ensure you have the following credentials in your configuration file.
Your API2CART key is 2afddb813193eb9d3b5bd99bf5d834cd, which you will need to access the API securely.
The following endpoints are available for your use:
- Get Products: https://api.api2cart.com/v1.0/products/get
- Add Product: https://api.api2cart.com/v1.0/products/add
`,
want: []string{"2afddb813193eb9d3b5bd99bf5d834cd"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{api2cart}{AQAAABAAA b36c17e9dc0dba67480e864cf69879c3}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"b36c17e9dc0dba67480e864cf69879c3"},
},
{
name: "invalid pattern",
input: `
To integrate with API2Cart, ensure you have the following credentials in your configuration file.
Your API2CART key is 68d746609J4240840734c22836725d76, which you will need to access the API securely.
The following endpoints are available for your use:
- Get Products: https://api.api2cart.com/v1.0/products/get
- Add Product: https://api.api2cart.com/v1.0/products/add
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/apideck/apideck.go
================================================
package apideck
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(sk_live_[a-z0-9A-Z-]{93})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"apideck"}) + `\b([a-z0-9A-Z]{40})\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{"apideck"}
}
// FromData will find and optionally verify ApiDeck 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)
var uniqueKeys, uniqueIds = make(map[string]struct{}), make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for _, matches := range idPat.FindAllStringSubmatch(dataStr, -1) {
uniqueIds[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
for id := range uniqueIds {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ApiDeck,
Raw: []byte(key),
RawV2: []byte(key + id),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAdobeIOSecret(ctx, client, key, id)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyAdobeIOSecret(ctx context.Context, client *http.Client, key string, id string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://unify.apideck.com/vault/consumers", nil)
if err != nil {
return false, err
}
req.Header.Add("x-apideck-app-id", id)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ApiDeck
}
func (s Scanner) Description() string {
return "ApiDeck is a platform that provides a unified API to connect multiple services. ApiDeck keys can be used to access and manage these services."
}
================================================
FILE: pkg/detectors/apideck/apideck_integration_test.go
================================================
//go:build detectors
// +build detectors
package apideck
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestApiDeck_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("APIDECK_TOKEN")
inactiveSecret := testSecrets.MustGetField("APIDECK_INACTIVE")
id := testSecrets.MustGetField("APIDECK_APPID")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apideck secret %s within apideckid %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ApiDeck,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apideck secret %s within apideckid %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ApiDeck,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("ApiDeck.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])
}
got[i].Raw = nil
if len(got[i].RawV2) == 0 {
t.Fatalf("no rawv2 secret present: \n %+v", got[i])
}
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("ApiDeck.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/apideck/apideck_test.go
================================================
package apideck
import (
"context"
"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"
)
func TestApiDeck_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the apideck API
[DEBUG] Using Key=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
[DEBUG] Using apideck ID=VfKE9Zh2ZatnqmrloqDu3PCnkNBR6Io4TlSbsG1P
[INFO] Response received: 200 OK
`,
want: []string{
"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",
},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{apideck id J6rYP2lzThxp9JeGg74TDgAXvfQsvzonsHpYHDsG}{apideck AQAAABAAA sk_live_R5S2B88smT6QfTsUc3o3DedI2hbbcnZwvQKjyudQ41V0T38L8qUDPUTlBDcVE2NwRp1PowPYqnmAHlZ-W1Yr7AWGvpCvT}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"sk_live_R5S2B88smT6QfTsUc3o3DedI2hbbcnZwvQKjyudQ41V0T38L8qUDPUTlBDcVE2NwRp1PowPYqnmAHlZ-W1Yr7AWGvpCvTJ6rYP2lzThxp9JeGg74TDgAXvfQsvzonsHpYHDsG"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the apideck API
[DEBUG] Using Key=sk_live_GKE08ADdkDV1DQ4vDfaW4ejDHy-TkotfxDmHvQMLX0HRvhtfPwku6olGvsG2vXBg869A0hsOPHHOw48SAF2GO7jBMs6Rt
[DEBUG] Using apideck ID=VfKE9Zh2ZatnqmrloqDu3PC_kNBR6Io4TlSbsG1P
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/apiflash/apiflash.go
================================================
package apiflash
import (
"context"
"fmt"
"io"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = detectors.DetectorHttpClientWithNoLocalAddresses
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"apiflash"}) + `\b([a-z0-9]{32})\b`)
urlToCapture = "http://google.com" // a fix constant url to capture to verify the access key
)
// 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{"apiflash"}
}
// FromData will find and optionally verify Apiflash 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)
uniqueAPIKeys := make(map[string]struct{})
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueAPIKeys[strings.TrimSpace(match[1])] = struct{}{}
}
for key := range uniqueAPIKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Apiflash,
Raw: []byte(key),
}
if verify {
isVerified, verificationErr := verifyAPIFlash(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, key)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Apiflash
}
func (s Scanner) Description() string {
return "Apiflash is a screenshot API service. Apiflash keys can be used to access and utilize the screenshot API service."
}
func verifyAPIFlash(ctx context.Context, client *http.Client, accessKey string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.apiflash.com/v1/urltoimage?url=%s&access_key=%s&wait_until=page_loaded", urlToCapture, accessKey), http.NoBody)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, nil
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/apiflash/apiflash_integration_test.go
================================================
//go:build detectors
// +build detectors
package apiflash
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestApiflash_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("APIFLASH")
url := testSecrets.MustGetField("API_URL")
inactiveSecret := testSecrets.MustGetField("APIFLASH_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apiflash secret %s within apiflash %s", secret, url)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Apiflash,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apiflash secret %s within apiflash %s but not valid", inactiveSecret, url)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Apiflash,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Apiflash.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Apiflash.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/apiflash/apiflash_test.go
================================================
package apiflash
import (
"context"
"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"
)
func TestApiFlash_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the apiflash API
[DEBUG] Using Key=grevetn5owrs1ybhxtcen0ibvg2mi85x
[INFO] Response received: 200 OK
`,
want: []string{"grevetn5owrs1ybhxtcen0ibvg2mi85x"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{apiflash}{AQAAABAAA axlzvcf9m7jyyts833f9gmtcpqe5b26o}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"axlzvcf9m7jyyts833f9gmtcpqe5b26o"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the apiflash API
[DEBUG] Using Key=grevetn5owRs1ybhxtcen0ibvg2mi85x
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/apifonica/apifonica.go
================================================
package apifonica
import (
"context"
b64 "encoding/base64"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"apifonica"}) + `\b([0-9a-z]{11}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})\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{"apifonica"}
}
// FromData will find and optionally verify Apifonica 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)
var uniqueKeys, uniqueTokens = make(map[string]struct{}), make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueTokens[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
for token := range uniqueTokens {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ApiFonica,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyApifonicaSecret(ctx, client, key, token)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyApifonicaSecret(ctx context.Context, client *http.Client, key string, token string) (bool, error) {
data := fmt.Sprintf("%s:%s", key, token)
sEnc := b64.StdEncoding.EncodeToString([]byte(data))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.apifonica.com/v2/accounts", nil)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ApiFonica
}
func (s Scanner) Description() string {
return "Apifonica is a cloud communication platform that provides APIs for messaging, voice, and other communication services. Apifonica keys can be used to access and manage these services."
}
================================================
FILE: pkg/detectors/apifonica/apifonica_integration_test.go
================================================
//go:build detectors
// +build detectors
package apifonica
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestApifonica_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("APIFONICA_SECRET")
token := testSecrets.MustGetField("APIFONICA_ID")
inactiveSecret := testSecrets.MustGetField("APIFONICA_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apifonica secret %s within apifonica token %s", secret, token)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ApiFonica,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apifonica secret %s within apifonica token %s but not valid", inactiveSecret, token)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ApiFonica,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Apifonica.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Apifonica.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/apifonica/apifonica_test.go
================================================
package apifonica
import (
"context"
"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"
)
func TestApiFonica_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the apifonica API
[DEBUG] Using Key=4rv0hdx5188-3q48-2luk-e8v5-dyuuf8l44ib7
[INFO] Response received: 200 OK
`,
want: []string{"4rv0hdx5188-3q48-2luk-e8v5-dyuuf8l44ib7"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{apifonica}{AQAAABAAA fvzlzj17xzz-lwon-842u-46bs-5spcl2g7u9eb}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"fvzlzj17xzz-lwon-842u-46bs-5spcl2g7u9eb"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the apifonica API
[DEBUG] Using Key=4rv0hdx51889-3q48-2luk-e8wv5-dyuuf8l44ib7
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/apify/apify.go
================================================
package apify
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(apify\_api\_[a-zA-Z-0-9]{36})\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{"apify"}
}
// FromData will find and optionally verify Apify 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Apify,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyApifyKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyApifyKey(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.apify.com/v2/acts?token="+key+"&my=true&offset=10&limit=99&desc=true", nil)
if err != nil {
return false, err
}
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Apify
}
func (s Scanner) Description() string {
return "Apify is a platform for web scraping and automation. Apify API keys can be used to access and control Apify actors and tasks."
}
================================================
FILE: pkg/detectors/apify/apify_integration_test.go
================================================
//go:build detectors
// +build detectors
package apify
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestApify_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("APIFY")
inactiveSecret := testSecrets.MustGetField("APIFY_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apify secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Apify,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apify secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Apify,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Apify.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Apify.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/apify/apify_test.go
================================================
package apify
import (
"context"
"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"
)
func TestApiFy_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using Key=apify_api_dXB1vLsglgTexUYm3JTAx2BHTjVuDBbvPl8R
[INFO] Response received: 200 OK
`,
want: []string{"apify_api_dXB1vLsglgTexUYm3JTAx2BHTjVuDBbvPl8R"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{user-id}{AQAAABAAA apify_api_RpTLEX9U18xfGl90wDaT2V9R-YX0TlMpxIzi}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"apify_api_RpTLEX9U18xfGl90wDaT2V9R-YX0TlMpxIzi"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using Key=apify_api_dXB1vLPglgTex_UYm3JTAx2BHTjVuDBbvPl8R
[INFO] Response received: 200 OK
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/apilayer/apilayer.go
================================================
package apilayer
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"apilayer"}) + `\b([a-zA-Z0-9]{32})\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{"apilayer"}
}
// FromData will find and optionally verify Apilayer 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Apilayer,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAPILayerKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyAPILayerKey(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.apilayer.com/number_verification/countries", nil)
if err != nil {
return false, err
}
req.Header.Add("apikey", key)
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Apilayer
}
func (s Scanner) Description() string {
return "Apilayer is a service providing various APIs for data verification and other utilities. Apilayer API keys can be used to access these services and perform operations such as number verification."
}
================================================
FILE: pkg/detectors/apilayer/apilayer_integration_test.go
================================================
//go:build detectors
// +build detectors
package apilayer
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestApilayer_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("APILAYER")
inactiveSecret := testSecrets.MustGetField("APILAYER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apilayer secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Apilayer,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apilayer secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Apilayer,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Apilayer.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Apilayer.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/apilayer/apilayer_test.go
================================================
package apilayer
import (
"context"
"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"
)
func TestApiLayer_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the apilayer API
[DEBUG] Using Key=qnHT110fihCn49wOm5b2h3ACTRmksbg0
[INFO] Response received: 200 OK
`,
want: []string{"qnHT110fihCn49wOm5b2h3ACTRmksbg0"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{apilayer}{AQAAABAAA HHTi3DYZIqt57j5WVHvXHHboXpCnm6CW}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"HHTi3DYZIqt57j5WVHvXHHboXpCnm6CW"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the apilayer API
[DEBUG] Using Key=qnHT110fiha-Cn49wOm5b2h3ACTRmksbg0
[INFO] Response received: 200 OK
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/apimatic/apimatic.go
================================================
package apimatic
import (
"context"
"fmt"
"io"
"net/http"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
apiKeyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"apimatic", "apikey"}) + `\b([a-zA-Z0-9_-]{64})\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{"apimatic"}
}
// FromData will find and optionally verify APIMatic 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)
uniqueApiKeys := make(map[string]struct{})
for _, matches := range apiKeyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueApiKeys[matches[1]] = struct{}{}
}
for apiKey := range uniqueApiKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_APIMatic,
Raw: []byte(apiKey),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAPImaticKey(ctx, client, apiKey)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyAPImaticKey(ctx context.Context, client *http.Client, key string) (bool, error) {
timeout := 10 * time.Second
client.Timeout = timeout
// api docs: https://docs.apimatic.io/platform-api/#/http/api-endpoints/code-generation-external-apis/list-all-code-generations
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.apimatic.io/code-generations", http.NoBody)
if err != nil {
return false, err
}
// authentication documentation: https://docs.apimatic.io/platform-api/#/http/guides/authentication
req.Header.Set("Authorization", "X-Auth-Key "+key)
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_APIMatic
}
func (s Scanner) Description() string {
return "APIMatic provides tools for generating SDKs, API documentation, and code snippets. APIMatic credentials can be used to access and manage these tools and services."
}
================================================
FILE: pkg/detectors/apimatic/apimatic_integration_test.go
================================================
//go:build detectors
// +build detectors
package apimatic
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAPIMatic_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("APIMATIC")
pass := testSecrets.MustGetField("APIMATIC_PASS")
inactiveSecret := testSecrets.MustGetField("APIMATIC_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apimatic secret %s within apimatic pass %s", secret, pass)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_APIMatic,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apimatic secret %s within apimatic pass %s but not valid", inactiveSecret, pass)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_APIMatic,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("APIMatic.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("APIMatic.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/apimatic/apimatic_test.go
================================================
package apimatic
import (
"context"
"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"
)
func TestApiMatic_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func validateApiMatic() bool {
apiMaticKey := "rc6iLoUEFGGAWNLsuBJnmsh4tZB-oCxcDUmc45HIPcuiQvfUEuqo8wb9YrUd2LyB"
// isActive check if the key is active or not
return isActive(apiMaticKey)
}`,
want: []string{"rc6iLoUEFGGAWNLsuBJnmsh4tZB-oCxcDUmc45HIPcuiQvfUEuqo8wb9YrUd2LyB"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{apimatic}{AQAAABAAA 2eqQBh9HkE-5Mq5Ma_vOEvvyt-x9shcZ-T5B7hSY1C5xvTl7qLMwGL6QAoNYmMcF}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"2eqQBh9HkE-5Mq5Ma_vOEvvyt-x9shcZ-T5B7hSY1C5xvTl7qLMwGL6QAoNYmMcF"},
},
{
name: "invalid pattern",
input: `
func validateApiMatic() bool {
apiMaticKey := "rc6iLoUEFGGAWNLsuBJnmsh4tZB@oCxcDUmc45HIPcuiQvfUEuqo8wb9YrUd2LyB"
// isActive check if the key is active or not
return isActive(apiMaticKey)
}`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/apimetrics/apimetrics.go
================================================
package apimetrics
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"apimetrics"}) + `\b([a-zA-Z0-9]{32})\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{"apimetrics"}
}
// FromData will find and optionally verify ApiMetrics 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ApiMetrics,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAPIMetricsKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyAPIMetricsKey(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://client.apimetrics.io/api/2/calls/", nil)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ApiMetrics
}
func (s Scanner) Description() string {
return "ApiMetrics is a tool for monitoring the performance of APIs. ApiMetrics keys can be used to access and manage API monitors."
}
================================================
FILE: pkg/detectors/apimetrics/apimetrics_integration_test.go
================================================
//go:build detectors
// +build detectors
package apimetrics
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestApiMetrics_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("APIMETRICS")
inactiveSecret := testSecrets.MustGetField("APIMETRICS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apimetrics secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ApiMetrics,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apimetrics secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ApiMetrics,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("ApiMetrics.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("ApiMetrics.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/apimetrics/apimetrics_test.go
================================================
package apimetrics
import (
"context"
"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"
)
func TestApiMetrics_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func validateApiMetrics() bool {
apiMetrics := "5po8TFGawiYNCc1ct4ofWkBqzIfA6IeO"
// isActive check if the key is active or not
return isActive(apiMetrics)
}`,
want: []string{"5po8TFGawiYNCc1ct4ofWkBqzIfA6IeO"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{apimetrics}{AQAAABAAA XpLTBFZccOgbbtVht4OaZzsrgKdh42RX}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"XpLTBFZccOgbbtVht4OaZzsrgKdh42RX"},
},
{
name: "invalid pattern",
input: `
func validateApiMetrics() bool {
apiMetrics := "5po8TFGawiYNCc1c4ofWkBqzIfA6IeO"
// isActive check if the key is active or not
return isActive(apiMetrics)
}`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/apitemplate/apitemplate.go
================================================
package apitemplate
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"apitemplate"}) + `\b([0-9a-zA-Z]{39})\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{"apitemplate"}
}
// FromData will find and optionally verify APITemplate 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_APITemplate,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyAPITemplateKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyAPITemplateKey(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.apitemplate.io/v1/list-templates", nil)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-API-KEY", key)
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_APITemplate
}
func (s Scanner) Description() string {
return "APITemplate is a service used to generate documents and images from templates. APITemplate API keys can be used to access and generate these documents and images."
}
================================================
FILE: pkg/detectors/apitemplate/apitemplate_integration_test.go
================================================
//go:build detectors
// +build detectors
package apitemplate
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAPITemplate_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("APITEMPLATE")
inactiveSecret := testSecrets.MustGetField("APITEMPLATE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apitemplate secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_APITemplate,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apitemplate secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_APITemplate,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("APITemplate.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("APITemplate.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/apitemplate/apitemplate_test.go
================================================
package apitemplate
import (
"context"
"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"
)
func TestApiTemplate_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func validateKey() bool {
apiTemplate := "EeOPHL7PyBlUk0qkJX72sDtdNL3WLdpxg1czllR"
// isActive check if the key is active or not
return isActive(apiTemplate)
}`,
want: []string{"EeOPHL7PyBlUk0qkJX72sDtdNL3WLdpxg1czllR"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{apitemplate}{AQAAABAAA oVqX8yfzlUtzudNnvlWKNI4pNKTKTwlaKmxlcX5}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"oVqX8yfzlUtzudNnvlWKNI4pNKTKTwlaKmxlcX5"},
},
{
name: "invalid pattern",
input: `
func validateKey() bool {
apiTemplate := "EeOPHL7PyBlUk0qkJAX72sDtdNL3WLdpxg1czllR"
// isActive check if the key is active or not
return isActive(apiTemplate)
}`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/apollo/apollo.go
================================================
package apollo
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"apollo"}) + `\b([a-zA-Z0-9]{22})\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{"apollo"}
}
// FromData will find and optionally verify Apollo 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Apollo,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyApolloKey(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyApolloKey(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.apollo.io/api/v1/mixed_people/search", nil)
if err != nil {
return false, err
}
req.Header.Add("x-api-key", key)
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.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Apollo
}
func (s Scanner) Description() string {
return "Apollo is a sales intelligence platform. Apollo API keys can be used to access and modify data within the Apollo platform."
}
================================================
FILE: pkg/detectors/apollo/apollo_integration_test.go
================================================
//go:build detectors
// +build detectors
package apollo
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestApollo_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("APOLLO")
inactiveSecret := testSecrets.MustGetField("APOLLO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apollo secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Apollo,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apollo secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Apollo,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Apollo.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Apollo.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/apollo/apollo_test.go
================================================
package apollo
import (
"context"
"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"
)
func TestApollo_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func validateApolloKey() bool {
apiKey := "897TJ1HevanW9Ye6nv6Ojj"
log.Println("Checking API key status...")
if !isActive(apiKey) {
log.Println("API key is inactive or invalid.")
return false
}
log.Println("API key is valid and active.")
return true
}`,
want: []string{"897TJ1HevanW9Ye6nv6Ojj"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{apollo}{AQAAABAAA S2wg2NMlgalg9AUsrXPd1O}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"S2wg2NMlgalg9AUsrXPd1O"},
},
{
name: "invalid pattern",
input: `
func validateApolloKey() bool {
apiKey := "897TJ1HevanW9Ye-nv6Ojj"
log.Println("Checking API key status...")
if !isActive(apiKey) {
log.Println("API key is inactive or invalid.")
return false
}
log.Println("API key is valid and active.")
return true
}`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/appcues/appcues.go
================================================
package appcues
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"appcues"}) + `\b([a-z0-9-]{36})\b`)
userPat = regexp.MustCompile(detectors.PrefixRegex([]string{"appcues"}) + `\b([a-z0-9-]{39})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"appcues"}) + `\b([0-9]{5})\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{"appcues"}
}
// FromData will find and optionally verify Appcues 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
userMatches := userPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, userMatch := range userMatches {
resUserMatch := strings.TrimSpace(userMatch[1])
for _, idMatch := range idMatches {
resIdMatch := strings.TrimSpace(idMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Appcues,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resUserMatch),
}
if verify {
isVerified, err := verifyMatch(ctx, client, resUserMatch, resMatch, resIdMatch)
s1.Verified = isVerified
s1.SetVerificationError(err, resUserMatch, resMatch, resIdMatch)
}
results = append(results, s1)
}
}
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, resUserMatch, resMatch, resIdMatch string) (bool, error) {
// Reference: https://api.appcues.com/v2/docs?_gl=1#responses
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://api.appcues.com/v2/accounts/%s/flows", resIdMatch), http.NoBody)
if err != nil {
return false, err
}
req.SetBasicAuth(resUserMatch, resMatch)
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.StatusUnauthorized, http.StatusForbidden, http.StatusBadRequest:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Appcues
}
func (s Scanner) Description() string {
return "Appcues is a user engagement platform that helps create personalized user experiences. The detected credentials can be used to access and manage user engagement flows and data."
}
================================================
FILE: pkg/detectors/appcues/appcues_integration_test.go
================================================
//go:build detectors
// +build detectors
package appcues
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAppcues_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("APPCUES")
user := testSecrets.MustGetField("APPCUES_USER")
id := testSecrets.MustGetField("APPCUES_ID")
inactiveSecret := testSecrets.MustGetField("APPCUES_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a appcues secret %s within appcues user %s and appcues id %s", secret, user, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Appcues,
Verified: true,
},
{
DetectorType: detectorspb.DetectorType_Appcues,
Verified: false,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a appcues secret %s within appcues user %s and appcues id %s but not valid", inactiveSecret, user, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Appcues,
Verified: false,
},
{
DetectorType: detectorspb.DetectorType_Appcues,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Appcues.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
for i := range got {
if len(got[i].Raw) == 0 {
t.Fatal("no raw secret present")
}
got[i].Raw = nil
if len(got[i].RawV2) == 0 {
t.Fatalf("no rawv2 secret present: \n %+v", got[i])
}
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Appcues.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/appcues/appcues_test.go
================================================
package appcues
import (
"context"
"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"
)
func TestAppCues_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the appcues API
[DEBUG] Using appcues Key=5g5n4yazu-dpqp3g6qt3gn59wrxhqf2mqipm
[DEBUG] Using appcues User=truffle-security-lrv10a8l4u23xp5gkvg819
[INFO] Response received: 200 OK
[INFO] APPCUES_ID=57843
`,
want: []string{"5g5n4yazu-dpqp3g6qt3gn59wrxhqf2mqipmtruffle-security-lrv10a8l4u23xp5gkvg819"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{appcues 91712}{appcues ubdcpht45hlfdywxv89ympnvtcnydl3uv-0umfu}{appcues AQAAABAAA w9hyyfghqirj8uwcmtv05-n4fppzl-in223u}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"w9hyyfghqirj8uwcmtv05-n4fppzl-in223uubdcpht45hlfdywxv89ympnvtcnydl3uv-0umfu"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the appcues API
[DEBUG] Using appcues Key=5g5n4yazu-dpqp3g6qt3gn59wrxhqf2mqipm
[DEBUG] Using appcues User=truffle_security-lrv10a8l4u23xp5gkvg819
[ERROR] Response received: 401 UnAuthorized
[INFO] ID=57843
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/appfollow/appfollow.go
================================================
package appfollow
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"appfollow"}) + `\b(eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9\.[0-9A-Za-z]{74}\.[0-9A-Z-a-z\-_]{43})\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{"appfollow"}
}
// FromData will find and optionally verify Appfollow 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Appfollow,
Raw: []byte(resMatch),
}
if verify {
isVerified, err := verifyMatch(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(err, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) {
// Reference: https://docs.api.appfollow.io/reference/users_list_api_v2_account_users_get-1
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.appfollow.io/api/v2/account/users", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("X-AppFollow-API-Token", 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, http.StatusPaymentRequired, http.StatusUnprocessableEntity:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Appfollow
}
func (s Scanner) Description() string {
return "Appfollow is a service used for app monitoring and analytics. Appfollow API tokens can be used to access and manage app data and analytics."
}
================================================
FILE: pkg/detectors/appfollow/appfollow_integration_test.go
================================================
//go:build detectors
// +build detectors
package appfollow
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAppfollow_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("APPFOLLOW")
inactiveSecret := testSecrets.MustGetField("APPFOLLOW_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a appfollow secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Appfollow,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a appfollow secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Appfollow,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Appfollow.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Appfollow.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/appfollow/appfollow_test.go
================================================
package appfollow
import (
"context"
"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"
)
func TestAppFollow_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func validateAppFollowKey() bool {
key := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.hdMLjiIayyb5cgbcVtjKywQwqeNKnsxZEhnJnX6wzhnblpmpjF4c2mbdmVVylTayE6M8ZE3h4V.fmnUM4cjvbe1JMFDuBSwWNEYQFHrD5AEm6p2Ir9w7K6"
// isActive check if the key is active or not
return isActive(key)
}`,
want: []string{"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.hdMLjiIayyb5cgbcVtjKywQwqeNKnsxZEhnJnX6wzhnblpmpjF4c2mbdmVVylTayE6M8ZE3h4V.fmnUM4cjvbe1JMFDuBSwWNEYQFHrD5AEm6p2Ir9w7K6"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{appfollow}{AQAAABAAA eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.YwK6gJ8sMVylaDNuXRiGFLRR1kgZaLF45EbJ0qHSRaW4CRtWaqWciTZZXxkk4a4wLh8f7cTTlb.wvTVCRC1RLCpd98q4WK3ef6M3TBrb08AkS9-jNOdA_r}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.YwK6gJ8sMVylaDNuXRiGFLRR1kgZaLF45EbJ0qHSRaW4CRtWaqWciTZZXxkk4a4wLh8f7cTTlb.wvTVCRC1RLCpd98q4WK3ef6M3TBrb08AkS9-jNOdA_r"},
},
{
name: "invalid pattern",
input: `
func validateAppFollowKey() bool {
apiKey := "eyJ0eXAiOiJKV1QiLCJhbGCiOiJIUzI1NiJ9.hdMLjiIayyb5cgbcVtjKywQwqeNKnsxZEhnJnX6wzhnblpmpjF4c2mbdVylTayE6M8ZE3h4V.fmnUM4cjvbe1JMFDuBSwWNEYQFHrDEm6p2Ir9w7K6"
log.Println("Checking API key status...")
if !isActive(apiKey) {
log.Println("API key is inactive or invalid.")
return false
}
log.Println("API key is valid and active.")
return true
}`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/appointedd/appointedd.go
================================================
package appointedd
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"appointedd"}) + `\b([a-zA-Z0-9=+]{88})`)
)
// 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{"appointedd"}
}
// FromData will find and optionally verify appointedd 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Appointedd,
Raw: []byte(resMatch),
}
if verify {
isVerified, err := verifyMatch(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(err, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, secret string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.appointedd.com/v1/availability/slots", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("X-API-KEY", secret)
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:
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
return strings.Contains(string(bodyBytes), "total"), nil
case http.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Appointedd
}
func (s Scanner) Description() string {
return "Appointedd provides online booking and scheduling services. The API key can be used to access and manage booking data."
}
================================================
FILE: pkg/detectors/appointedd/appointedd_integration_test.go
================================================
//go:build detectors
// +build detectors
package appointedd
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAppointedd_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("APPOINTEDD")
inactiveSecret := testSecrets.MustGetField("APPOINTEDD_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a appointedd secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Appointedd,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a appointedd secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Appointedd,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Appointedd.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Appointedd.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/appointedd/appointedd_test.go
================================================
package appointedd
import (
"context"
"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"
)
func TestAppFollow_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func validateAppointeddKey() bool {
appointeddKey := "Ci0a2bSpRyFcZyEXBEr9RHzf3xXllqO=XVoh+t0L0s8T2s3MFntfWhBlovqLaqEadtuJ9=Jy6yCOXmhbpEZPfY7Y"
log.Println("Checking API key status...")
if !isActive(appointeddKey) {
log.Println("API key is inactive or invalid.")
return false
}
log.Println("API key is valid and active.")
return true
}`,
want: []string{"Ci0a2bSpRyFcZyEXBEr9RHzf3xXllqO=XVoh+t0L0s8T2s3MFntfWhBlovqLaqEadtuJ9=Jy6yCOXmhbpEZPfY7Y"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{appointedd}{AQAAABAAA 2pRMKW=JrG9+xYmqlJMa4Omf9goqsSqsM3mIaqG8tG4lwnVrKIslbn=IpLIz7GTDEJUcQ0wlr6B+UjfvSY9XKXwu}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"2pRMKW=JrG9+xYmqlJMa4Omf9goqsSqsM3mIaqG8tG4lwnVrKIslbn=IpLIz7GTDEJUcQ0wlr6B+UjfvSY9XKXwu"},
},
{
name: "invalid pattern",
input: `
func validateAppointeddKey() bool {
appointeddKey := "Ci0a2bSpRyFcZyEXBEr9RHzf3xXllqO-XVoh+t0L0s8T2s3MFntfWhBlovqLaqEadtuJ9-Jy6yCOXmhbpEZPfY7Y"
log.Println("Checking API key status...")
if !isActive(appointeddKey) {
log.Println("API key is inactive or invalid.")
return false
}
log.Println("API key is valid and active.")
return true
}`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/appoptics/appoptics.go
================================================
package appoptics
import (
"context"
b64 "encoding/base64"
"fmt"
regexp "github.com/wasilibs/go-re2"
"net/http"
"strings"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"appoptics"}) + `\b([0-9a-zA-Z_-]{71})\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{"appoptics"}
}
// FromData will find and optionally verify Appoptics 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AppOptics,
Raw: []byte(resMatch),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.appoptics.com/v1/metrics", nil)
if err != nil {
continue
}
data := fmt.Sprintf("%s:", resMatch)
sEnc := b64.StdEncoding.EncodeToString([]byte(data))
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
} else if res.StatusCode == 401 {
// The secret is determinately not verified (nothing to do)
} else {
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
s1.SetVerificationError(err, resMatch)
}
} else {
s1.SetVerificationError(err, resMatch)
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AppOptics
}
func (s Scanner) Description() string {
return "AppOptics is a cloud-based infrastructure monitoring service. AppOptics API keys can be used to access and manage monitoring data and configurations."
}
================================================
FILE: pkg/detectors/appoptics/appoptics_integration_test.go
================================================
//go:build detectors
// +build detectors
package appoptics
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAppoptics_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("APPOPTICS")
inactiveSecret := testSecrets.MustGetField("APPOPTICS_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a appoptics secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AppOptics,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a appoptics secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AppOptics,
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,
},
}
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("Appoptics.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Appoptics.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/appoptics/appoptics_test.go
================================================
package appoptics
import (
"context"
"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"
)
func TestAppOptics_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func validateAppOpticsKey() bool {
appopticsKey := "Xwl4ViaAFDLrAmFX9g1blkUVC5dJj2he3a1tzkpJ4-PznQukQruRjqMFbEG73L92LJyBGMZ"
log.Println("Checking API key status...")
if !isActive(appopticsKey) {
log.Println("API key is inactive or invalid.")
return false
}
log.Println("API key is valid and active.")
return true
}`,
want: []string{"Xwl4ViaAFDLrAmFX9g1blkUVC5dJj2he3a1tzkpJ4-PznQukQruRjqMFbEG73L92LJyBGMZ"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{appoptics}{AQAAABAAA zxsb8yzT0RbIJ1TAalB87LOVUcT1b4uEgvT4tXCcSqv_gcmlrx5aQRleHPDFKePjpHFof5J}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"zxsb8yzT0RbIJ1TAalB87LOVUcT1b4uEgvT4tXCcSqv_gcmlrx5aQRleHPDFKePjpHFof5J"},
},
{
name: "invalid pattern",
input: `
func validateAppOpticsKey() bool {
appopticsKey := "Xwl4ViaAFDLrAmFX9g1blkUVC5dJj2h:3a1tzkpJ43PznQukQruRjqMFbEG73L92LJyBGMZ"
log.Println("Checking API key status...")
if !isActive(appopticsKey) {
log.Println("API key is inactive or invalid.")
return false
}
log.Println("API key is valid and active.")
return true
}`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/appsynergy/appsynergy.go
================================================
package appsynergy
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"appsynergy"}) + `\b([a-z0-9]{64})\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{"appsynergy"}
}
// FromData will find and optionally verify AppSynergy 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AppSynergy,
Raw: []byte(resMatch),
}
if verify {
isVerified, err := verifyMatch(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(err, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, secret string) (bool, error) {
payload := strings.NewReader(`{"html":"
Hello World
","filename":"HelloWorld.pdf"}`)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://www.appsynergy.com/api?action=HTML2PDF&apiKey="+secret, payload)
if err != nil {
return false, err
}
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.StatusUnauthorized:
return false, nil
case http.StatusBadRequest:
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
body := string(bodyBytes)
if strings.Contains(body, "Invalid API Key") {
return false, nil
}
return false, fmt.Errorf("status bad request invalid api key message not found: %d", res.StatusCode)
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AppSynergy
}
func (s Scanner) Description() string {
return "AppSynergy is a platform for building cloud applications. AppSynergy API keys can be used to access and manage applications and data within the platform."
}
================================================
FILE: pkg/detectors/appsynergy/appsynergy_integration_test.go
================================================
//go:build detectors
// +build detectors
package appsynergy
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAppSynergy_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("APPSYNERGY")
inactiveSecret := testSecrets.MustGetField("APPSYNERGY_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a appsynergy secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AppSynergy,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a appsynergy secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AppSynergy,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AppSynergy.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("AppSynergy.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/appsynergy/appsynergy_test.go
================================================
package appsynergy
import (
"context"
"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"
)
func TestAppSynergy_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func validateAppSynergyKey() bool {
appSyneregyKey := "mg1pgwlndtq7rbk8i3kum344aso8ggp02ximdhsp8nsqasd3btxf84lz9ekfdpwo"
log.Println("Checking API key status...")
if !isActive(appSyneregyKey) {
log.Println("API key is inactive or invalid.")
return false
}
log.Println("API key is valid and active.")
return true
}`,
want: []string{"mg1pgwlndtq7rbk8i3kum344aso8ggp02ximdhsp8nsqasd3btxf84lz9ekfdpwo"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{appsynergy}{AQAAABAAA ri1vn9m2otlg3yi8wwjegltc1t3bi4ljogg6c80onnrox2t9fuim6tce430fhklz}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"ri1vn9m2otlg3yi8wwjegltc1t3bi4ljogg6c80onnrox2t9fuim6tce430fhklz"},
},
{
name: "invalid pattern",
input: `
func validateAppSynergyKey() bool {
appSyneregyKey := "mg1pgwlndtq7rbk8i3kum_44aso8ggp02ximdhsp8nsqasd3btxf84lz9ekfdpwo"
log.Println("Checking API key status...")
if !isActive(appSyneregyKey) {
log.Println("API key is inactive or invalid.")
return false
}
log.Println("API key is valid and active.")
return true
}`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/apptivo/apptivo.go
================================================
package apptivo
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"apptivo"}) + `\b([a-z0-9-]{36})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"apptivo"}) + `\b([a-zA-Z0-9-]{32})\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{"apptivo"}
}
// FromData will find and optionally verify Apptivo 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
resIdMatch := strings.TrimSpace(idMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Apptivo,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch),
}
if verify {
isVerified, err := verifyMatch(ctx, client, resMatch, resIdMatch)
s1.Verified = isVerified
s1.SetVerificationError(err, resMatch, resIdMatch)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, apiKey, accessKey string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://api.apptivo.com/app/dao/v6/leads?a=getConfigData&apiKey=%s&accessKey=%s", apiKey, accessKey), http.NoBody)
if err != nil {
return false, err
}
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:
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
return strings.Contains(string(bodyBytes), `displayName`), nil
default:
return false, fmt.Errorf("unexpected status code %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Apptivo
}
func (s Scanner) Description() string {
return "Apptivo is a cloud-based suite of business solutions, including CRM, project management, and more. Apptivo API keys can be used to access and manage these services programmatically."
}
================================================
FILE: pkg/detectors/apptivo/apptivo_integration_test.go
================================================
//go:build detectors
// +build detectors
package apptivo
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestApptivo_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("APPTIVO")
id := testSecrets.MustGetField("APPTIVO_KEY")
inactiveSecret := testSecrets.MustGetField("APPTIVO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apptivo secret %s within apptivo %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Apptivo,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a apptivo secret %s within but not valid apptivo %s", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Apptivo,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Apptivo.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Apptivo.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/apptivo/apptivo_test.go
================================================
package apptivo
import (
"context"
"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"
)
func TestApptivo_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the apptivo API
[DEBUG] Using apptivo Key=fox94at7-8dj92ns-cdxhag4470yqp0o2c8y
[DEBUG] Using apptivo ID=C27YfQFKcUue8OxfEiAcqzrPVII-pb3V
[INFO] Response received: 200 OK
`,
want: []string{"fox94at7-8dj92ns-cdxhag4470yqp0o2c8yC27YfQFKcUue8OxfEiAcqzrPVII-pb3V"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{apptivo o9qB77Q9cCXfuV-TWyCWUumiAbZc2Z7i}{apptivo AQAAABAAA juqc5-sw846p0cj43wy8eex6rr4v8-9oa3dh}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"juqc5-sw846p0cj43wy8eex6rr4v8-9oa3dho9qB77Q9cCXfuV-TWyCWUumiAbZc2Z7i"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the apptivo API
[DEBUG] Using apptivo Key=fOx94aT7-8dj92ns-cdxhag4470yqp0o2c8y
[DEBUG] Using apptivo ID=C27YfQF-cUue8OxfEiAcqzrPVII-pb3V
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/artifactory/artifactory.go
================================================
package artifactory
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
detectors.EndpointSetter
}
var (
// Ensure the Scanner satisfies the interface at compile time.
_ detectors.Detector = (*Scanner)(nil)
_ detectors.EndpointCustomizer = (*Scanner)(nil)
defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(AKCp[a-zA-Z0-9]{69})\b`)
URLPat = regexp.MustCompile(`\b([A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]\.jfrog\.io)`)
invalidHosts = simple.NewCache[struct{}]()
errNoHost = errors.New("no such host")
)
func (Scanner) CloudEndpoint() string { return "" }
// 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{"artifactory", "jfrog.io", "AKCp"}
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Artifactory 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)
var uniqueTokens, uniqueUrls = make(map[string]struct{}), make(map[string]struct{})
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueTokens[match[1]] = struct{}{}
}
var foundUrls = make([]string, 0)
for _, match := range URLPat.FindAllStringSubmatch(dataStr, -1) {
foundUrls = append(foundUrls, match[1])
}
// add found + configured endpoints to the list
for _, endpoint := range s.Endpoints(foundUrls...) {
// if any configured endpoint has `https://` remove it because we append that during verification
endpoint = strings.TrimPrefix(endpoint, "https://")
uniqueUrls[endpoint] = struct{}{}
}
for token := range uniqueTokens {
for url := range uniqueUrls {
if invalidHosts.Exists(url) {
delete(uniqueUrls, url)
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ArtifactoryAccessToken,
Raw: []byte(token),
RawV2: []byte(token + url),
}
if verify {
isVerified, verificationErr := verifyArtifactory(ctx, s.getClient(), url, token)
s1.Verified = isVerified
if verificationErr != nil {
if errors.Is(verificationErr, errNoHost) {
invalidHosts.Set(url, struct{}{})
continue
}
s1.SetVerificationError(verificationErr, token)
if isVerified {
s1.AnalysisInfo = map[string]string{
"domain": url,
"token": token,
}
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func verifyArtifactory(ctx context.Context, client *http.Client, resURLMatch, resMatch string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://"+resURLMatch+"/artifactory/api/system/ping", nil)
if err != nil {
return false, err
}
req.Header.Add("X-JFrog-Art-Api", resMatch)
resp, err := client.Do(req)
if err != nil {
// lookup foo.jfrog.io: no such host
if strings.Contains(err.Error(), "no such host") {
return false, errNoHost
}
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, err
}
if strings.Contains(string(body), "OK") {
return true, nil
}
return false, nil
case http.StatusUnauthorized, http.StatusForbidden, http.StatusFound: // 302 can occur if the url is incorrect
// https://jfrog.com/help/r/jfrog-rest-apis/error-responses
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ArtifactoryAccessToken
}
func (s Scanner) Description() string {
return "Artifactory is a repository manager that supports all major package formats. Artifactory access tokens can be used to authenticate and perform operations on repositories."
}
================================================
FILE: pkg/detectors/artifactory/artifactory_integration_test.go
================================================
//go:build detectors
// +build detectors
package artifactory
import (
"context"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestArtifactory_FromChunk(t *testing.T) {
// NOTE: Using mock secrets because JFrog deprecated AKCp API keys (disabled creation end of Q3 2024).
// Real AKCp keys can no longer be generated, so we cannot test actual verification scenarios.
// These mock keys follow the correct format: AKCp + 69 alphanumeric characters = 73 total
// Reference: https://jfrog.com/help/r/jfrog-release-information/artifactory-7.47.10-cloud-self-hosted
mockSecret := "AKCp5bueTFpfypEqQbGJPp7eHFi28fBivfWczrjbPb9erDff9LbXZbj6UsRExVXA8asWGc9fM"
appURL := "trufflehog.jfrog.io"
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, unverified - mock key (cannot verify deprecated AKCp format)",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a artifactory secret %s and domain %s", mockSecret, appURL)),
verify: false, // Cannot verify - AKCp API keys are deprecated and no valid keys available
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ArtifactoryAccessToken,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.s.UseFoundEndpoints(true)
got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Artifactory.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", "primarySecret")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Artifactory.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/artifactory/artifactory_test.go
================================================
package artifactory
import (
"context"
"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"
)
func TestArtifactory_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
cloudEndpoint string
useCloudEndpoint bool
useFoundEndpoint bool
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the artifactory API
[DEBUG] Using Key=AKCp5e2gMx8TtJNDtrsuPq7Jz24Rqjkjf1d1iiy1GuEjmdsY8ghxFGgehZcK3UGNgy5TxHWdE
[INFO] rwxtOp.jfrog.io
[INFO] Response received: 200 OK
`,
useCloudEndpoint: false,
useFoundEndpoint: true,
want: []string{
"AKCp5e2gMx8TtJNDtrsuPq7Jz24Rqjkjf1d1iiy1GuEjmdsY8ghxFGgehZcK3UGNgy5TxHWdE" +
"rwxtOp.jfrog.io",
},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{artifactory}AKCp8budTFpbypBqQbGJPp7eHFi28fBivfWczrjbPb9erDff9LbXZbj6UsRExVXA8asWGc9fM{HTTPnGQZ79vjWXze.jfrog.io}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
useCloudEndpoint: false,
useFoundEndpoint: true,
want: []string{
"AKCp8budTFpbypBqQbGJPp7eHFi28fBivfWczrjbPb9erDff9LbXZbj6UsRExVXA8asWGc9fM" +
"HTTPnGQZ79vjWXze.jfrog.io",
},
},
{
name: "valid pattern - with cloud endpoints",
input: `
[INFO] Sending request to the artifactory API
[DEBUG] Using Key=AKCp5e2gMx8TtJNDtrsuPq7Jz24Rqjkjf1d1iiy1GuEjmdsY8ghxFGgehZcK3UGNgy5TxHWdE
[INFO] Response received: 200 OK
`,
cloudEndpoint: "cloudendpoint.jfrog.io",
useCloudEndpoint: true,
useFoundEndpoint: false,
want: []string{
"AKCp5e2gMx8TtJNDtrsuPq7Jz24Rqjkjf1d1iiy1GuEjmdsY8ghxFGgehZcK3UGNgy5TxHWdE" +
"cloudendpoint.jfrog.io",
},
},
{
name: "valid pattern - with cloud and found endpoints",
input: `
[INFO] Sending request to the artifactory API
[DEBUG] Using Key=AKCp5e2gMx8TtJNDtrsuPq7Jz24Rqjkjf1d1iiy1GuEjmdsY8ghxFGgehZcK3UGNgy5TxHWdE
[INFO] rwxtOp.jfrog.io
[INFO] Response received: 200 OK
`,
cloudEndpoint: "cloudendpoint.jfrog.io",
useCloudEndpoint: true,
useFoundEndpoint: true,
want: []string{
"AKCp5e2gMx8TtJNDtrsuPq7Jz24Rqjkjf1d1iiy1GuEjmdsY8ghxFGgehZcK3UGNgy5TxHWdE" +
"cloudendpoint.jfrog.io",
"AKCp5e2gMx8TtJNDtrsuPq7Jz24Rqjkjf1d1iiy1GuEjmdsY8ghxFGgehZcK3UGNgy5TxHWdE" +
"rwxtOp.jfrog.io",
},
},
{
name: "valid pattern - with disabled found endpoints",
input: `
[INFO] Sending request to the artifactory API
[DEBUG] Using Key=AKCp5e2gMx8TtJNDtrsuPq7Jz24Rqjkjf1d1iiy1GuEjmdsY8ghxFGgehZcK3UGNgy5TxHWdE
[INFO] rwxtOp.jfrog.io
[INFO] Response received: 200 OK
`,
cloudEndpoint: "cloudendpoint.jfrog.io",
useCloudEndpoint: true,
useFoundEndpoint: false,
want: []string{
"AKCp5e2gMx8TtJNDtrsuPq7Jz24Rqjkjf1d1iiy1GuEjmdsY8ghxFGgehZcK3UGNgy5TxHWdE" +
"cloudendpoint.jfrog.io",
},
},
{
name: "valid pattern - with https in configured endpoint",
input: `
[INFO] Sending request to the artifactory API
[DEBUG] Using Key=AKCp5e2gMx8TtJNDtrsuPq7Jz24Rqjkjf1d1iiy1GuEjmdsY8ghxFGgehZcK3UGNgy5TxHWdE
[INFO] Response received: 200 OK
`,
cloudEndpoint: "https://cloudendpoint.jfrog.io",
useCloudEndpoint: true,
useFoundEndpoint: false,
want: []string{
"AKCp5e2gMx8TtJNDtrsuPq7Jz24Rqjkjf1d1iiy1GuEjmdsY8ghxFGgehZcK3UGNgy5TxHWdE" +
"cloudendpoint.jfrog.io",
},
},
{
name: "invalid pattern - wrong prefix",
input: `
[INFO] Sending request to the artifactory API
[DEBUG] Using Key=XYZp5e2gMx8TtJNDtrsuPq7Jz24Rqjkjf1d1iiy1GuEjmdsY8ghxFGgehZcK3UGNgy5TxHWdE
[INFO] rwxtOp.jfrog.io
[INFO] Response received: 200 OK
`,
useFoundEndpoint: true,
want: nil,
},
{
name: "invalid pattern - too short",
input: `
[INFO] Sending request to the artifactory API
[DEBUG] Using Key=AKCp5e2gMx8TtJNDtrsuPq7Jz24Rqjkjf1d1iiy1GuEjmd
[INFO] rwxtOp.jfrog.io
[INFO] Response received: 200 OK
`,
useFoundEndpoint: true,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// this detector uses endpoint customizer interface so we need to enable them based on test case
d.UseFoundEndpoints(test.useFoundEndpoint)
d.UseCloudEndpoint(test.useCloudEndpoint)
// if the test case provides cloud endpoint, then use it
if test.useCloudEndpoint && test.cloudEndpoint != "" {
d.SetCloudEndpoint(test.cloudEndpoint)
}
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/artifactoryreferencetoken/artifactoryreferencetoken.go
================================================
package artifactoryreferencetoken
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
detectors.EndpointSetter
}
var (
// Ensure the Scanner satisfies the interface at compile time.
_ detectors.Detector = (*Scanner)(nil)
_ detectors.EndpointCustomizer = (*Scanner)(nil)
defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses
// Reference tokens are base64-encoded strings starting with "reftkn:01|::"
// The base64 encoding of "reftkn" is "cmVmdGtu", total length is always 64 characters
tokenPat = regexp.MustCompile(`\b(cmVmdGtu[A-Za-z0-9]{56})\b`)
urlPat = regexp.MustCompile(`\b([A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]\.jfrog\.io)`)
invalidHosts = simple.NewCache[struct{}]()
errNoHost = errors.New("no such host")
)
func (Scanner) CloudEndpoint() string { return "" }
// Keywords are used for efficiently pre-filtering chunks.
func (s Scanner) Keywords() []string {
return []string{"cmVmdGtu"}
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Artifactory Reference tokens 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)
var uniqueTokens, uniqueUrls = make(map[string]struct{}), make(map[string]struct{})
for _, match := range tokenPat.FindAllStringSubmatch(dataStr, -1) {
uniqueTokens[match[1]] = struct{}{}
}
foundUrls := make([]string, 0)
for _, match := range urlPat.FindAllStringSubmatch(dataStr, -1) {
foundUrls = append(foundUrls, match[1])
}
// Add found + configured endpoints to the list
for _, endpoint := range s.Endpoints(foundUrls...) {
// If any configured endpoint has `https://` remove it because we append that during verification
endpoint = strings.TrimPrefix(endpoint, "https://")
uniqueUrls[endpoint] = struct{}{}
}
for token := range uniqueTokens {
for url := range uniqueUrls {
if invalidHosts.Exists(url) {
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ArtifactoryReferenceToken,
Raw: []byte(token),
RawV2: []byte(token + url),
}
if verify {
isVerified, verificationErr := verifyToken(ctx, s.getClient(), url, token)
s1.Verified = isVerified
if verificationErr != nil {
if errors.Is(verificationErr, errNoHost) {
invalidHosts.Set(url, struct{}{})
continue
}
s1.SetVerificationError(verificationErr, token)
}
if isVerified {
s1.AnalysisInfo = map[string]string{
"domain": url,
"token": token,
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func verifyToken(ctx context.Context, client *http.Client, host, token string) (bool, error) {
// https://jfrog.com/help/r/jfrog-rest-apis/get-token-by-id
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
"https://"+host+"/access/api/v1/tokens/me", http.NoBody)
if err != nil {
return false, err
}
req.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(req)
if err != nil {
if strings.Contains(err.Error(), "no such host") {
return false, errNoHost
}
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
// JFrog returns 200 with HTML for invalid subdomains, so we need to check Content-Type
contentType := resp.Header.Get("Content-Type")
if strings.Contains(contentType, "application/json") {
return true, nil
}
// HTML response indicates invalid subdomain/redirect - treat as invalid host
return false, errNoHost
case http.StatusForbidden:
// 403 - the authenticated principal has no permissions to get the token
return true, nil
case http.StatusUnauthorized:
// 401 - invalid/expired token
return false, nil
default:
// 404 - endpoint not found (possibly wrong URL or old Artifactory version)
// 302 and 500+
return false, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ArtifactoryReferenceToken
}
func (s Scanner) Description() string {
return "JFrog Artifactory is a binary repository manager. Reference tokens are 64-character access tokens that can be used to authenticate API requests, providing access to repositories, builds, and artifacts."
}
================================================
FILE: pkg/detectors/artifactoryreferencetoken/artifactoryreferencetoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package artifactoryreferencetoken
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/detectorspb"
)
func TestArtifactoryreferencetoken_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
instanceURL := testSecrets.MustGetField("ARTIFACTORY_URL")
secret := testSecrets.MustGetField("ARTIFACTORYREFERENCETOKEN")
inactiveSecret := testSecrets.MustGetField("ARTIFACTORYREFERENCETOKEN_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a artifactoryreferencetoken secret %s and domain %s within", secret, instanceURL)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ArtifactoryReferenceToken,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a artifactoryreferencetoken secret %s and domain %s within but not valid", inactiveSecret, instanceURL)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ArtifactoryReferenceToken,
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 artifactoryreferencetoken secret %s and domain %s within", secret, instanceURL)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ArtifactoryReferenceToken,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: true,
},
{
name: "found, verified but unexpected api surface",
s: Scanner{client: common.ConstantResponseHttpClient(302, "")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a artifactoryreferencetoken secret %s and domain %s within", secret, instanceURL)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ArtifactoryReferenceToken,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.s.UseFoundEndpoints(true)
got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Artifactoryreferencetoken.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "primarySecret", "AnalysisInfo")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Artifactoryreferencetoken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/artifactoryreferencetoken/artifactoryreferencetoken_test.go
================================================
package artifactoryreferencetoken
import (
"context"
"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"
)
func TestArtifactoryReferenceToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
cloudEndpoint string
useCloudEndpoint bool
useFoundEndpoint bool
want []string
}{
{
name: "valid pattern - environment variable",
input: `
[INFO] Connecting to Artifactory
[DEBUG] Using reference token: cmVmdGtuOjAxOjAwMDAwMDAwMDA6awJQVlZkdEVyWXJ2cVNSemAABVQ1bwaBSWtE
[INFO] Connected to trufflehog.jfrog.io
`,
useCloudEndpoint: false,
useFoundEndpoint: true,
want: []string{
"cmVmdGtuOjAxOjAwMDAwMDAwMDA6awJQVlZkdEVyWXJ2cVNSemAABVQ1bwaBSWtEtrufflehog.jfrog.io",
},
},
{
name: "valid pattern - config file",
input: `
artifactory:
url: https://trufflehog.jfrog.io
reference_token: cmVmdGtuOjAxOjE3NjkxNjY0NjE6RE2ZeXpdsU1sOENUUG1RqXqDawNeMrJaTapu
`,
useCloudEndpoint: false,
useFoundEndpoint: true,
want: []string{
"cmVmdGtuOjAxOjE3NjkxNjY0NjE6RE2ZeXpdsU1sOENUUG1RqXqDawNeMrJaTaputrufflehog.jfrog.io",
},
},
{
name: "valid pattern - curl command",
input: `
curl -H "Authorization: Bearer cmVmdGtuOjAxOjE3NzE0OTkzNzY6RG9OS0QxOHVLduRyyUtNrneMwqt6a33TNUZV" \
https://trufflehog.jfrog.io/artifactory/api/system/ping
`,
useCloudEndpoint: false,
useFoundEndpoint: true,
want: []string{
"cmVmdGtuOjAxOjE3NzE0OTkzNzY6RG9OS0QxOHVLduRyyUtNrneMwqt6a33TNUZVtrufflehog.jfrog.io",
},
},
{
name: "valid pattern - with cloud endpoint",
input: `
[INFO] Connecting to Artifactory
[DEBUG] Using reference token: cmVmdGtuOjAxOjAwMDAwMDAwMDA6awJQVlZkdEVyWXJ2cVNSemAABVQ1bwaBSWtE
[INFO] Response received: 200 OK
`,
cloudEndpoint: "cloudendpoint.jfrog.io",
useCloudEndpoint: true,
useFoundEndpoint: false,
want: []string{
"cmVmdGtuOjAxOjAwMDAwMDAwMDA6awJQVlZkdEVyWXJ2cVNSemAABVQ1bwaBSWtEcloudendpoint.jfrog.io",
},
},
{
name: "valid pattern - with cloud and found endpoints",
input: `
[INFO] Connecting to Artifactory
[DEBUG] Using reference token: cmVmdGtuOjAxOjAwMDAwMDAwMDA6awJQVlZkdEVyWXJ2cVNSemAABVQ1bwaBSWtE
[INFO] trufflehog.jfrog.io
[INFO] Response received: 200 OK
`,
cloudEndpoint: "cloudendpoint.jfrog.io",
useCloudEndpoint: true,
useFoundEndpoint: true,
want: []string{
"cmVmdGtuOjAxOjAwMDAwMDAwMDA6awJQVlZkdEVyWXJ2cVNSemAABVQ1bwaBSWtEcloudendpoint.jfrog.io",
"cmVmdGtuOjAxOjAwMDAwMDAwMDA6awJQVlZkdEVyWXJ2cVNSemAABVQ1bwaBSWtEtrufflehog.jfrog.io",
},
},
{
name: "valid pattern - with disabled found endpoints",
input: `
[INFO] Connecting to Artifactory
[DEBUG] Using reference token: cmVmdGtuOjAxOjAwMDAwMDAwMDA6awJQVlZkdEVyWXJ2cVNSemAABVQ1bwaBSWtE
[INFO] trufflehog.jfrog.io
[INFO] Response received: 200 OK
`,
cloudEndpoint: "cloudendpoint.jfrog.io",
useCloudEndpoint: true,
useFoundEndpoint: false,
want: []string{
"cmVmdGtuOjAxOjAwMDAwMDAwMDA6awJQVlZkdEVyWXJ2cVNSemAABVQ1bwaBSWtEcloudendpoint.jfrog.io",
},
},
{
name: "valid pattern - with https in configured endpoint",
input: `
[INFO] Connecting to Artifactory
[DEBUG] Using reference token: cmVmdGtuOjAxOjAwMDAwMDAwMDA6awJQVlZkdEVyWXJ2cVNSemAABVQ1bwaBSWtE
[INFO] Response received: 200 OK
`,
cloudEndpoint: "https://cloudendpoint.jfrog.io",
useCloudEndpoint: true,
useFoundEndpoint: false,
want: []string{
"cmVmdGtuOjAxOjAwMDAwMDAwMDA6awJQVlZkdEVyWXJ2cVNSemAABVQ1bwaBSWtEcloudendpoint.jfrog.io",
},
},
{
name: "finds multiple tokens",
input: `
# Primary token
export ARTIFACTORY_TOKEN=cmVmdGtuOjAxOjAwMDAwMDAwMDA6awJQVlZkdEVyWXJ2cVNSemAABVQ1bwaBSWtE
# Backup token
export ARTIFACTORY_TOKEN_BACKUP=cmVmdGtuOjAxOjE3NjkxNjY0NjE6RE2ZeXpdsU1sOENUUG1RqXqDawNeMrJaTapu
export ARTIFACTORY_URL=https://trufflehog.jfrog.io
`,
useCloudEndpoint: false,
useFoundEndpoint: true,
want: []string{
"cmVmdGtuOjAxOjAwMDAwMDAwMDA6awJQVlZkdEVyWXJ2cVNSemAABVQ1bwaBSWtEtrufflehog.jfrog.io",
"cmVmdGtuOjAxOjE3NjkxNjY0NjE6RE2ZeXpdsU1sOENUUG1RqXqDawNeMrJaTaputrufflehog.jfrog.io",
},
},
{
name: "invalid pattern - too short",
input: `
[DEBUG] Using token: cmVmdGtuOjAxOjAwMDAwMDAwMDA6SHORT
[INFO] URL: trufflehog.jfrog.io
`,
useCloudEndpoint: false,
useFoundEndpoint: true,
want: nil,
},
{
name: "invalid pattern - wrong prefix",
input: `
[DEBUG] Using token: aBcDeFgHOjAxOjAwMDAwMDAwMDA6awJQVlZkdEVyWXJ2cVNSemAABVQ1bwaBSWtE
[INFO] URL: trufflehog.jfrog.io
`,
useCloudEndpoint: false,
useFoundEndpoint: true,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Configure endpoint customizer based on test case
d.UseFoundEndpoints(test.useFoundEndpoint)
d.UseCloudEndpoint(test.useCloudEndpoint)
if test.useCloudEndpoint && test.cloudEndpoint != "" {
d.SetCloudEndpoint(test.cloudEndpoint)
}
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 && len(test.want) > 0 {
t.Errorf("keywords were not matched: %v", d.Keywords())
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))
for _, r := range results {
t.Logf("got: %s", string(r.RawV2))
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/artsy/artsy.go
================================================
package artsy
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"artsy"}) + `\b([0-9a-zA-Z]{32})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"artsy"}) + `\b([0-9a-zA-Z]{20})\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{"artsy"}
}
// FromData will find and optionally verify Artsy 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idmatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idmatch := range idmatches {
resIdMatch := strings.TrimSpace(idmatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Artsy,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch),
}
if verify {
isVerified, err := verifyMatch(ctx, client, resIdMatch, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(err, resMatch)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, id, secret string) (bool, error) {
// Reference: https://developers.artsy.net/v2/docs/authentication
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.artsy.net/api/tokens/xapp_token?client_id="+id+"&client_secret="+secret, http.NoBody)
if err != nil {
return false, err
}
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.StatusCreated:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Artsy
}
func (s Scanner) Description() string {
return "Artsy is an online platform for discovering, buying, and selling art. Artsy API keys can be used to access Artsy's services and data."
}
================================================
FILE: pkg/detectors/artsy/artsy_integration_test.go
================================================
//go:build detectors
// +build detectors
package artsy
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestArtsy_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ARTSY")
inactiveSecret := testSecrets.MustGetField("ARTSY_INACTIVE")
id := testSecrets.MustGetField("ARTSY_CLIENTID")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a artsy secret %s within artsyid %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Artsy,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a artsy secret %s within artsyid %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Artsy,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Artsy.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])
}
got[i].Raw = nil
if len(got[i].RawV2) == 0 {
t.Fatalf("no rawv2 secret present: \n %+v", got[i])
}
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Artsy.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/artsy/artsy_test.go
================================================
package artsy
import (
"context"
"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"
)
func TestArtsy_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the artsy API
[DEBUG] Using Key=rU0K6hwGw9AeANtXrZ8FQJT9jn4sRdlj
[DEBUG] Using artsy ID=hvQ2fMvUPNczDCdmzi0i
[INFO] Response received: 200 OK
`,
want: []string{"rU0K6hwGw9AeANtXrZ8FQJT9jn4sRdljhvQ2fMvUPNczDCdmzi0i"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{artsy Mbw4Tihfv1ttrspD1yXk}{artsy AQAAABAAA 3V4gtw8ZmDShAfzq2KKb3w0gZODnzxp7}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"3V4gtw8ZmDShAfzq2KKb3w0gZODnzxp7Mbw4Tihfv1ttrspD1yXk"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the artsy API
[DEBUG] Using Key=rU0K6hwGw9AeANtX-Z8FQJT9jn4sRdlj
[DEBUG] Using artsy ID=hvQ2fMvUPN_zDCdmzi0i
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/asanaoauth/asanaoauth.go
================================================
package asanaoauth
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"asana"}) + `\b([a-z\/:0-9]{51})\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{"asana"}
}
// FromData will find and optionally verify AsanaOauth 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AsanaOauth,
Raw: []byte(resMatch),
}
if verify {
isVerified, err := verifyMatch(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(err, resMatch)
s1.AnalysisInfo = map[string]string{"key": resMatch}
}
results = append(results, s1)
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://app.asana.com/api/1.0/users/me", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", 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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AsanaOauth
}
func (s Scanner) Description() string {
return "Asana is a work management platform that helps teams organize, track, and manage their work. Asana OAuth tokens can be used to access and interact with Asana's API on behalf of a user."
}
================================================
FILE: pkg/detectors/asanaoauth/asanaoauth_integration_test.go
================================================
//go:build detectors
// +build detectors
package asanaoauth
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAsanaOauth_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ASANAOAUTH_TOKEN")
inactiveSecret := testSecrets.MustGetField("ASANAOAUTH_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a asana secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AsanaOauth,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a asana secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AsanaOauth,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AsanaOauth.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])
}
got[i].Raw = nil
got[i].AnalysisInfo = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("AsanaOauth.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/asanaoauth/asanaoauth_test.go
================================================
package asanaoauth
import (
"context"
"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"
)
func TestAsanaOauth_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the asana API
[DEBUG] Using Key=q5przi0tmp6xpo7rpsd0q:kl0qg:2gdj3jyumq04q9kcqk/qxdo
[INFO] Response received: 200 OK
`,
want: []string{"q5przi0tmp6xpo7rpsd0q:kl0qg:2gdj3jyumq04q9kcqk/qxdo"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{asana}{AQAAABAAA omzmg54nn5wa21sh6qwg:dos10bfl1f6vnqcs9lcdwkbqb68gti}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"omzmg54nn5wa21sh6qwg:dos10bfl1f6vnqcs9lcdwkbqb68gti"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the asana API
[DEBUG] Using Key=q5przi0tmP6xpo7rpsd0q;kl0qg:2gdj3jyumq04q9kcqk/qxdo
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/asanapersonalaccesstoken/asanapersonalaccesstoken.go
================================================
package asanapersonalaccesstoken
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Updated pattern to handle both old and new token formats
// Old format: [digits]/[16+ digits]:[32+ chars]
// New format: [digits]/[16+ digits]/[16+ digits]:[32+ chars]
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"asana"}) + `\b([0-9]{1,}\/[0-9]{16,}(?:\/[0-9]{16,})?:[A-Za-z0-9]{32,})\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{"asana"}
}
// FromData will find and optionally verify AsanaPersonalAccessToken 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AsanaPersonalAccessToken,
Raw: []byte(resMatch),
}
if verify {
isVerified, err := verifyMatch(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(err, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://app.asana.com/api/1.0/users/me", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", 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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AsanaPersonalAccessToken
}
func (s Scanner) Description() string {
return "Asana is a web and mobile application designed to help teams organize, track, and manage their work. Asana Personal Access Tokens can be used to access and modify data within Asana."
}
================================================
FILE: pkg/detectors/asanapersonalaccesstoken/asanapersonalaccesstoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package asanapersonalaccesstoken
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAsanaPersonalAccessToken_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
testNewSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
oldFormatSecret := testSecrets.MustGetField("ASANA_PAT")
newFormatSecret := testNewSecrets.MustGetField("ASANA_PAT_NEW")
inactiveOldFormatSecret := testSecrets.MustGetField("ASANA_PAT_INACTIVE")
inactiveNewFormatSecret := testNewSecrets.MustGetField("ASANA_PAT_NEW_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a asana secret %s within", oldFormatSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AsanaPersonalAccessToken,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a asana secret %s within but unverified", inactiveSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AsanaPersonalAccessToken,
Verified: false,
},
},
wantErr: false,
},
{
name: "found, verified - new format",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a asana secret %s within", newFormatSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AsanaPersonalAccessToken,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified - new format",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a asana secret %s but unverified", inactiveNewFormatSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AsanaPersonalAccessToken,
Verified: false,
},
},
wantErr: 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,
},
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AsanaPersonalAccessToken.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("AsanaPersonalAccessToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/asanapersonalaccesstoken/asanapersonalaccesstoken_test.go
================================================
package asanapersonalaccesstoken
import (
"context"
"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"
)
func TestAsanaPersonalAccessToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern - old format",
input: `
[INFO] Sending request to the asana API
[DEBUG] Using Old Format asana Key=5947/1724908107002616220416212965:Yv3DoiSFhtsgUwN3AcnXWjK8zabQHKSHBRHpuNKVjz3oCcpyDIdXRm3GL4SUDkTMFoTb
[ERROR] Response received: 400 BadRequest
[DEBUG] Using new format asana Key=7/9823746598123746/8923746598123456:7f1a3c9be84d2a6c4e7d9c32bf1e7f88
[INFO] Response received: 200 OK
`,
want: []string{
"5947/1724908107002616220416212965:Yv3DoiSFhtsgUwN3AcnXWjK8zabQHKSHBRHpuNKVjz3oCcpyDIdXRm3GL4SUDkTMFoTb",
"7/9823746598123746/8923746598123456:7f1a3c9be84d2a6c4e7d9c32bf1e7f88",
},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{asana}{AQAAABAAA 891435852083139681602524390768273271357927849104481/366163755073364840345913922341185329292536814045275090976491644844014597476863956806652784056747/17480879147700616278211801017829125:Hb7meGPLBz7jH7e1fiHetN355omiO9Zt8fewjSOX4qfUoWDzvvlNA6lBx9rNuR8EAEElmtmmL9J4ilO8m2D56n}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"891435852083139681602524390768273271357927849104481/366163755073364840345913922341185329292536814045275090976491644844014597476863956806652784056747/17480879147700616278211801017829125:Hb7meGPLBz7jH7e1fiHetN355omiO9Zt8fewjSOX4qfUoWDzvvlNA6lBx9rNuR8EAEElmtmmL9J4ilO8m2D56n"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the asana API
[DEBUG] Using Old Format asana Key=5947766540345/172490810700261:Yv3DoiSFhjK8zabQHKSHBRHpuNKVjz3oCcpyDIdXRm3GL4SUDkTMFoTbRDCHe8tTBHxdtoXItn
[ERROR] Response received: 400 BadRequest
[DEBUG] Using new format asana Key=7/98237465/8923746598156:7f1a3c9be84d2a6c4e7d9c32bf1e7f88
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/assemblyai/assemblyai.go
================================================
package assemblyai
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"assemblyai"}) + `\b([0-9a-z]{32})\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{"assemblyai"}
}
// FromData will find and optionally verify Assemblyai 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AssemblyAI,
Raw: []byte(resMatch),
}
if verify {
isVerified, err := verifyMatch(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(err, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.assemblyai.com/v2/transcript", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Authorization", 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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AssemblyAI
}
func (s Scanner) Description() string {
return "AssemblyAI is a service that provides speech-to-text transcription. AssemblyAI keys can be used to access and utilize the transcription services provided by AssemblyAI."
}
================================================
FILE: pkg/detectors/assemblyai/assemblyai_integration_test.go
================================================
//go:build detectors
// +build detectors
package assemblyai
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAssemblyai_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ASSEMBLYAI")
inactiveSecret := testSecrets.MustGetField("ASSEMBLYAI_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a assemblyai secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AssemblyAI,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a assemblyai secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AssemblyAI,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Assemblyai.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Assemblyai.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/assemblyai/assemblyai_test.go
================================================
package assemblyai
import (
"context"
"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"
)
func TestAssemblyAI_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using assemblyai Key=mlhekyjhs96mx0r2cxbzky4jzr83fw1q
[INFO] Response received: 200 OK
`,
want: []string{"mlhekyjhs96mx0r2cxbzky4jzr83fw1q"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{assemblyai}{AQAAABAAA s0c8a99g0w6qbwybdxn4uowzemk1xlca}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"s0c8a99g0w6qbwybdxn4uowzemk1xlca"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using assemblyai Key=Mlhekyjzr83fw1qr2cxbzky4jzr83f1q
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/atera/atera.go
================================================
package atera
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"atera"}) + `\b([0-9a-z]{32})\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{"atera"}
}
// FromData will find and optionally verify Atera 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Atera,
Raw: []byte(resMatch),
}
if verify {
isVerified, err := verifyMatch(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(err, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://app.atera.com/api/v3/alerts", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("X-API-KEY", 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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Atera
}
func (s Scanner) Description() string {
return "Atera is an IT management platform that provides remote monitoring and management for IT professionals. Atera API keys can be used to interact with the Atera API to manage alerts, tickets, devices, and more."
}
================================================
FILE: pkg/detectors/atera/atera_integration_test.go
================================================
//go:build detectors
// +build detectors
package atera
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAtera_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ATERA")
inactiveSecret := testSecrets.MustGetField("ATERA_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find an atera secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Atera,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find an atera secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Atera,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Atera.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Atera.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/atera/atera_test.go
================================================
package atera
import (
"context"
"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"
)
func TestAtera_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the atera API
[DEBUG] Using Key=yoo3d5pu3t4zxd6x1vhk7ykmjqarbsv1
[INFO] Response received: 200 OK
`,
want: []string{"yoo3d5pu3t4zxd6x1vhk7ykmjqarbsv1"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{atera}{AQAAABAAA uvyn0qy0ec96pgxfr2s3i4bqv1znl7yg}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"uvyn0qy0ec96pgxfr2s3i4bqv1znl7yg"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the atera API
[DEBUG] Using Key=yOO3d5pu3t4zxd6x1vhk7ykmjqarbs_1
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/atlassian/v1/atlassian.go
================================================
package atlassian
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/detectorspb"
)
type Scanner struct {
client *http.Client
}
func (s Scanner) Version() int { return 1 }
type OrgRes struct {
Data []struct {
Attributes struct {
Name string `json:"name"`
} `json:"attributes"`
} `json:"data"`
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"atlassian"}) + `\b([a-zA-Z-0-9]{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{"atlassian"}
}
// Description returns a description for the result being detected
func (s Scanner) Description() string {
return "Atlassian provides tools for software development, project management, and content management. Atlassian API keys can be used to access and manage these tools and services."
}
// FromData will find and optionally verify Atlassian 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 match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Atlassian,
Raw: []byte(match),
ExtraData: map[string]string{
"rotation_guide": "https://howtorotate.com/docs/tutorials/atlassian/",
"version": fmt.Sprintf("%d", s.Version()),
},
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, orgResponse, verificationErr := verifyMatch(ctx, client, match)
s1.Verified = isVerified
if orgResponse != nil {
s1.ExtraData["Organization"] = orgResponse.Data[0].Attributes.Name
}
s1.SetVerificationError(verificationErr, match)
if isVerified {
s1.AnalysisInfo = map[string]string{
"key": match,
}
}
}
results = append(results, s1)
}
return
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, *OrgRes, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.atlassian.com/admin/v1/orgs", nil)
if err != nil {
return false, nil, err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("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()
}()
switch res.StatusCode {
case http.StatusOK:
// If the endpoint returns useful information, we can return it as a map.
var orgResponse OrgRes
if err = json.NewDecoder(res.Body).Decode(&orgResponse); err != nil {
return false, nil, err
}
return true, &orgResponse, nil
case http.StatusUnauthorized:
// The secret is determinately not verified (nothing to do)
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Atlassian
}
================================================
FILE: pkg/detectors/atlassian/v1/atlassian_integration_test.go
================================================
//go:build detectors
// +build detectors
package atlassian
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/detectorspb"
)
func TestAtlassian_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("ATLASSIAN")
inactiveSecret := testSecrets.MustGetField("ATLASSIAN_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a atlassian secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Atlassian,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a atlassian secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Atlassian,
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 atlassian secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Atlassian,
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 atlassian secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Atlassian,
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("Atlassian.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "ExtraData")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Atlassian.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/atlassian/v1/atlassian_test.go
================================================
package atlassian
import (
"context"
"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"
)
func TestAtlassian_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the atlassian API
[DEBUG] Using Key=aB1cD2eF3gH4iJ5kL6mN7oP8
[INFO] Response received: 200 OK
`,
want: []string{"aB1cD2eF3gH4iJ5kL6mN7oP8"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{atlassian}{AQAAABAAA r6RkiQao3PgqY9MOKtonpJdU}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"r6RkiQao3PgqY9MOKtonpJdU"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/atlassian/v2/atlassian.go
================================================
package atlassian
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/detectorspb"
)
type Scanner struct {
client *http.Client
}
func (s Scanner) Version() int { return 2 }
type OrgRes struct {
Data []struct {
Attributes struct {
Name string `json:"name"`
} `json:"attributes"`
} `json:"data"`
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
// Example: ATCTT3xFfGN0GsZNgOGrQSHSnxiJVi00oHlRicyM0yMNuKCBfw6qOHVcCy4Hm89GnclGb_W-1qAkxqCn5XbuyoX54bNhpK5yFKGFR7ocV6FByvL_P9Sb3tFnbUg3T3I3S_RGCBLMSN7Nsa4GJv8JEJ6bzvDmX-oJ8AnrazMU-zZ5hb-u3t2ERew=366BFE3A
keyPat = regexp.MustCompile(`\b(ATCTT3xFfG[A-Za-z0-9+/=_-]+=[A-Za-z0-9]{8})\b`)
// Example: 123e4567-e89b-12d3-a456-426614174000
organizationIdPat = regexp.MustCompile(detectors.PrefixRegex([]string{"org", "id"}) + `\b([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})\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{"ATCTT3xFfG"}
}
// Description returns a description for the result being detected
func (s Scanner) Description() string {
return "Atlassian is a software company that provides tools for project management, software development, and collaboration. Atlassian tokens can be used to access and manage these tools and services."
}
// FromData will find and optionally verify Atlassian 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{}{}
}
uniqueOrgIdMatches := make(map[string]struct{})
for _, match := range organizationIdPat.FindAllStringSubmatch(dataStr, -1) {
uniqueOrgIdMatches[match[1]] = struct{}{}
}
if len(uniqueOrgIdMatches) == 0 {
// we only need an org ID to pass into AnalysisInfo
// if we don't find one, we can still verify the key
// we can add a dummy entry here just to make sure a result is returned
uniqueOrgIdMatches[""] = struct{}{}
}
for match := range uniqueMatches {
for orgId := range uniqueOrgIdMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Atlassian,
Raw: []byte(match),
ExtraData: map[string]string{
"rotation_guide": "https://howtorotate.com/docs/tutorials/atlassian/",
"version": fmt.Sprintf("%d", s.Version()),
},
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, orgResponse, verificationErr := verifyMatch(ctx, client, match)
s1.Verified = isVerified
if orgResponse != nil && len(orgResponse.Data) > 0 {
s1.ExtraData["Organization"] = orgResponse.Data[0].Attributes.Name
}
s1.SetVerificationError(verificationErr, match)
if s1.Verified {
s1.AnalysisInfo = map[string]string{
"key": match,
}
if orgId != "" {
s1.AnalysisInfo["organization_id"] = orgId
}
}
}
results = append(results, s1)
}
}
return
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, *OrgRes, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.atlassian.com/admin/v1/orgs", nil)
if err != nil {
return false, nil, err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("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()
}()
switch res.StatusCode {
case http.StatusOK:
// If the endpoint returns useful information, we can return it as a map.
var orgResponse OrgRes
if err = json.NewDecoder(res.Body).Decode(&orgResponse); err != nil {
return false, nil, err
}
return true, &orgResponse, nil
case http.StatusUnauthorized:
// The secret is determinately not verified (nothing to do)
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Atlassian
}
================================================
FILE: pkg/detectors/atlassian/v2/atlassian_integration_test.go
================================================
//go:build detectors
// +build detectors
package atlassian
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/detectorspb"
)
func TestAtlassian_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("ATLASSIAN")
inactiveSecret := testSecrets.MustGetField("ATLASSIAN_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a atlassian secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Atlassian,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a atlassian secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Atlassian,
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 atlassian secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Atlassian,
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 atlassian secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Atlassian,
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("Atlassian.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "ExtraData", "primarySecret", "AnalysisInfo")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Atlassian.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/atlassian/v2/atlassian_test.go
================================================
package atlassian
import (
"context"
"fmt"
"net/http"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
"gopkg.in/h2non/gock.v1"
)
func TestAtlassian_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the atlassian API
[DEBUG] Using Key=ATCTT3xFfGN0GsZNgOGrQSHSnxiJVi00oHlRicyM0yMNuKCBfw6qOHVcCy4Hm89GnclGb_W-1qAkxqCn5XbuyoX54bNhpK5yFKGFR7ocV6FByvL_P9Sb3tFnbUg3T3I3S_RGCBLMSN7Nsa4GJv8JEJ6bzvDmX-oJ8AnrazMU-zZ5hb-u3t2ERew=366BFE3A
[INFO] Response received: 200 OK
`,
want: []string{"ATCTT3xFfGN0GsZNgOGrQSHSnxiJVi00oHlRicyM0yMNuKCBfw6qOHVcCy4Hm89GnclGb_W-1qAkxqCn5XbuyoX54bNhpK5yFKGFR7ocV6FByvL_P9Sb3tFnbUg3T3I3S_RGCBLMSN7Nsa4GJv8JEJ6bzvDmX-oJ8AnrazMU-zZ5hb-u3t2ERew=366BFE3A"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{98651}{AQAAABAAA ATCTT3xFfGXc59Vkq40qLX=iEOIrJRZ}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"ATCTT3xFfGXc59Vkq40qLX=iEOIrJRZ"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
// TestAtlassian_AnalysisInfo_KeyAndOrgId tests if both the key and organization id are populated into AnalysisInfo
// given that they are present in the input data chunk
func TestAtlassian_AnalysisInfo_KeyAndOrgId(t *testing.T) {
client := common.SaneHttpClient()
d := Scanner{client: client}
key := "ATCTT3xFfGN0GsZNgOGrQSHSnxiJVi00oHlRicyM0yMNuKCBfw6qOHVcCy4Hm89GnclGb_W-1qAkxqCn5XbuyoX54bNhpK5yFKGFR7ocV6FByvL_P9Sb3tFnbUg3T3I3S_RGCBLMSN7Nsa4GJv8JEJ6bzvDmX-oJ8AnrazMU-zZ5hb-u3t2ERew=366BFE3A"
orgId := "123j4567-e89b-12d3-a456-426614174000"
defer gock.Off()
defer gock.RestoreClient(client)
gock.InterceptClient(client)
gock.New("https://api.atlassian.com").
Get("/admin/v1/orgs").
MatchHeader("Accept", "application/json").
MatchHeader("Authorization", fmt.Sprintf("Bearer %s", key)).
Reply(http.StatusOK).
JSON(map[string]any{
"Data": []map[string]any{},
})
t.Run("key and organization id both present", func(t *testing.T) {
input := fmt.Sprintf(`
[INFO] Sending request to the atlassian API
[DEBUG] Using Key=%s
[DEBUG] Using Organization ID=%s
[INFO] Response received: 200 OK
`, key, orgId)
results, err := d.FromData(context.Background(), true, []byte(input))
require.NoError(t, err)
require.Len(t, results, 1, "mismatch in result count: expected %d, got %d", 1, len(results))
result := results[0]
require.NotNil(t, result.AnalysisInfo, "AnalysisInfo is nil")
assert.Equal(t, key, result.AnalysisInfo["key"], "mismatch in key")
assert.Equal(t, orgId, result.AnalysisInfo["organization_id"], "mismatch in organization_id")
})
}
// TestAtlassian_AnalysisInfo_KeyOnly tests if only key is populated into AnalysisInfo
// given that only the key and no organization_id is present in the input data chunk
func TestAtlassian_AnalysisInfo_KeyOnly(t *testing.T) {
client := common.SaneHttpClient()
d := Scanner{client: client}
key := "ATCTT3xFfGN0GsZNgOGrQSHSnxiJVi00oHlRicyM0yMNuKCBfw6qOHVcCy4Hm89GnclGb_W-1qAkxqCn5XbuyoX54bNhpK5yFKGFR7ocV6FByvL_P9Sb3tFnbUg3T3I3S_RGCBLMSN7Nsa4GJv8JEJ6bzvDmX-oJ8AnrazMU-zZ5hb-u3t2ERew=366BFE3A"
defer gock.Off()
defer gock.RestoreClient(client)
gock.InterceptClient(client)
gock.New("https://api.atlassian.com").
Get("/admin/v1/orgs").
MatchHeader("Accept", "application/json").
MatchHeader("Authorization", fmt.Sprintf("Bearer %s", key)).
Reply(http.StatusOK).
JSON(map[string]any{
"Data": []map[string]any{},
})
t.Run("only key present", func(t *testing.T) {
input := fmt.Sprintf(`
[INFO] Sending request to the atlassian API
[DEBUG] Using Key=%s
[INFO] Response received: 200 OK
`, key)
results, err := d.FromData(context.Background(), true, []byte(input))
require.NoError(t, err)
require.Len(t, results, 1, "mismatch in result count: expected %d, got %d", 1, len(results))
result := results[0]
require.NotNil(t, result.AnalysisInfo, "AnalysisInfo is nil")
assert.Equal(t, key, result.AnalysisInfo["key"], "mismatch in key")
})
}
================================================
FILE: pkg/detectors/audd/audd.go
================================================
package audd
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"audd"}) + `\b([a-z0-9-]{32})\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{"audd"}
}
// FromData will find and optionally verify Audd 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Audd,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.audd.io/setCallbackUrl/?api_token=%s&url=https://yourwebsite.com/callbacks_handler/", resMatch), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
bodyBytes, err := io.ReadAll(res.Body)
if err == nil {
bodyString := string(bodyBytes)
validResponse := strings.Contains(bodyString, `"status":"success"`)
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
if validResponse {
s1.Verified = true
} else {
s1.Verified = false
}
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Audd
}
func (s Scanner) Description() string {
return "Audd is a music recognition service. Audd API tokens can be used to access the Audd API services for recognizing music and obtaining metadata."
}
================================================
FILE: pkg/detectors/audd/audd_integration_test.go
================================================
//go:build detectors
// +build detectors
package audd
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAudd_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AUDD")
inactiveSecret := testSecrets.MustGetField("AUDD_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a audd secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Audd,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a audd secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Audd,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Audd.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Audd.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/audd/audd_test.go
================================================
package audd
import (
"context"
"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"
)
func TestAudd_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the audd API
[DEBUG] Using Key=60fzzcspq2balbxn7f3hi2nvg3h07h4z
[INFO] Response received: 200 OK
`,
want: []string{"60fzzcspq2balbxn7f3hi2nvg3h07h4z"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{audd}{AQAAABAAA uv2kv0x8htfhgnugnsbys7a8oyky5ryb}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"uv2kv0x8htfhgnugnsbys7a8oyky5ryb"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the audd API
[DEBUG] Using Key=60fzzcspq2balbxn7f3hi2nvg3h07h4zY
[INFO] Response received: 200 OK
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/auth0managementapitoken/auth0managementapitoken.go
================================================
package auth0managementapitoken
import (
"context"
"fmt"
"io"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.MaxSecretSizeProvider = (*Scanner)(nil)
var (
client = detectors.DetectorHttpClientWithLocalAddresses
// long jwt token but note this is default 8640000 seconds = 24 hours but could be set to maximum 2592000 seconds = 720 hours = 30 days
// at https://manage.auth0.com/dashboard/us/dev-63memjo3/apis/management/explorer
managementAPITokenPat = regexp.MustCompile(`\b(ey[a-zA-Z0-9._-]+)\b`)
domainPat = regexp.MustCompile(`([a-zA-Z0-9\-]{2,16}\.[a-zA-Z0-9_-]{2,3}\.auth0\.com)`) // could be part of url
)
// 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{"auth0"} }
const maxSecretSize = 5000
func (Scanner) MaxSecretSize() int64 { return maxSecretSize }
// FromData will find and optionally verify Auth0ManagementApiToken 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)
managementAPITokenMatches := managementAPITokenPat.FindAllStringSubmatch(dataStr, -1)
domainMatches := domainPat.FindAllStringSubmatch(dataStr, -1)
for _, managementApiTokenMatch := range managementAPITokenMatches {
managementAPITokenRes := strings.TrimSpace(managementApiTokenMatch[1])
if len(managementAPITokenRes) < 2000 || len(managementAPITokenRes) > 5000 {
continue
}
for _, domainMatch := range domainMatches {
domainRes := strings.TrimSpace(domainMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Auth0ManagementApiToken,
Redacted: domainRes,
Raw: []byte(managementAPITokenRes),
RawV2: []byte(managementAPITokenRes + domainRes),
}
if verify {
isVerified, err := verifyMatch(ctx, client, managementAPITokenRes, domainRes)
s1.Verified = isVerified
s1.SetVerificationError(err, managementAPITokenRes)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, token, domain string) (bool, error) {
/*
curl -H "Authorization: Bearer $token" https://domain/api/v2/users
Reference: https://auth0.com/docs/api/management/v2/users/get-users
*/
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://"+domain+"/api/v2/users", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", 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, http.StatusForbidden:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Auth0ManagementApiToken
}
func (s Scanner) Description() string {
return "Auth0 provides authentication and authorization as a service. Auth0 Management API tokens can be used to manage users, roles, permissions, and other aspects of the Auth0 service."
}
================================================
FILE: pkg/detectors/auth0managementapitoken/auth0managementapitoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package auth0managementapitoken
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAuth0ManagementApiToken_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
// use 2592000 for 30 days, this is the maximum allowed
managementApiToken := testSecrets.MustGetField("AUTH0_MANAGEMENT_APITOKEN")
inactiveManagementApiToken := testSecrets.MustGetField("AUTH0_MANAGEMENT_APITOKEN_INACTIVE")
domain := testSecrets.MustGetField("AUTH0_MANAGEMENT_DOMAIN")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a auth0 secret %s domain %s", managementApiToken, domain)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Auth0ManagementApiToken,
RawV2: []byte(managementApiToken + domain),
Redacted: domain,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a auth0 secret %s domain https://%s/oauth/token within but not valid", inactiveManagementApiToken, domain)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Auth0ManagementApiToken,
RawV2: []byte(inactiveManagementApiToken + domain),
Redacted: domain,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Auth0ManagementApiToken.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Auth0ManagementApiToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/auth0managementapitoken/auth0managementapitoken_test.go
================================================
package auth0managementapitoken
import (
"context"
"fmt"
"math/rand"
"strings"
"testing"
"time"
"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 (
// TODO: Refactor the fake token generation if possible
validPattern = generateRandomString() // this has the exact token string only which can be used in want too
)
func TestAuth0ManagementApitToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: makeFakeTokenString(validPattern, "Truffle-security.org.auth0.com"),
want: []string{validPattern + "Truffle-security.org.auth0.com"},
},
{
name: "invalid pattern",
input: `
auth0_credentials:
apiToken: eywT2nGMZwOcbsUVBwfiRPEl8P_wnmo6XfdUoGVwxDfOSjNyqhYqFdi.KojZZOM8Ox
domain: Truffle-security.org.auth0.com
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
// makeFakeTokenString take a string token as parameter and make a string that looks like a token for testing
func makeFakeTokenString(token, domain string) string {
return fmt.Sprintf("auth0:\n apiToken: %s \n domain: %s", token, domain)
}
// generateRandomString generates exactly 2001 char string for a fake token to pass the check in detector for testing
func generateRandomString() string {
const length = 2001
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
const charsetWithBoundaryChars = charset + ".-"
random := rand.New(rand.NewSource(time.Now().UnixNano()))
var builder strings.Builder
builder.Grow(length)
for i := 0; i < length-1; i++ {
randomChar := charsetWithBoundaryChars[random.Intn(len(charset))]
builder.WriteByte(randomChar)
}
// ensure last character is not boundary character
lastChar := charset[random.Intn(len(charset))]
builder.WriteByte(lastChar)
// append ey in start as the token must start with 'ey'
return fmt.Sprintf("ey%s", builder.String())
}
================================================
FILE: pkg/detectors/auth0oauth/auth0oauth.go
================================================
package auth0oauth
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = detectors.DetectorHttpClientWithLocalAddresses
clientIdPat = regexp.MustCompile(detectors.PrefixRegex([]string{"auth0"}) + `\b([a-zA-Z0-9_-]{32,60})\b`)
clientSecretPat = regexp.MustCompile(`\b([a-zA-Z0-9_-]{64,})\b`)
domainPat = regexp.MustCompile(`\b([a-zA-Z0-9][a-zA-Z0-9._-]*auth0\.com)\b`) // could be part of url
)
// 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{"auth0"}
}
// FromData will find and optionally verify Auth0oauth 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)
uniqueDomainMatches := make(map[string]struct{})
uniqueClientIDs := make(map[string]struct{})
uniqueSecrets := make(map[string]struct{})
for _, m := range domainPat.FindAllStringSubmatch(dataStr, -1) {
uniqueDomainMatches[strings.TrimSpace(m[1])] = struct{}{}
}
for _, m := range clientIdPat.FindAllStringSubmatch(dataStr, -1) {
uniqueClientIDs[strings.TrimSpace(m[1])] = struct{}{}
}
for _, m := range clientSecretPat.FindAllStringSubmatch(dataStr, -1) {
uniqueSecrets[strings.TrimSpace(m[1])] = struct{}{}
}
for clientIdRes := range uniqueClientIDs {
for clientSecretRes := range uniqueSecrets {
for domainRes := range uniqueDomainMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Auth0oauth,
Redacted: clientIdRes,
Raw: []byte(clientSecretRes),
RawV2: []byte(clientIdRes + clientSecretRes),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, err := verifyTuple(ctx, client, domainRes, clientIdRes, clientSecretRes)
if err != nil {
s1.SetVerificationError(err, clientIdRes)
}
s1.Verified = isVerified
}
results = append(results, s1)
}
}
}
return results, nil
}
func verifyTuple(ctx context.Context, client *http.Client, domainRes, clientId, clientSecret string) (bool, error) {
/*
curl --request POST \
--url 'https://YOUR_DOMAIN/oauth/token' \
--header 'content-type: application/x-www-form-urlencoded' \
--data 'grant_type=authorization_code&client_id=W44JmL3qD6LxHeEJyKe9lMuhcwvPOaOq&client_secret=YOUR_CLIENT_SECRET&code=AUTHORIZATION_CODE&redirect_uri=undefined'
*/
data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("client_id", clientId)
data.Set("client_secret", clientSecret)
data.Set("code", "AUTHORIZATION_CODE")
data.Set("redirect_uri", "undefined")
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://"+domainRes+"/oauth/token", strings.NewReader(data.Encode())) // URL-encoded payload
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
// This condition will never meet due to invalid request body
return true, nil
case http.StatusUnauthorized:
return false, nil
case http.StatusForbidden:
// cross check about 'invalid_grant' or 'unauthorized_client' in response body
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return false, err
}
bodyStr := string(bodyBytes)
if strings.Contains(bodyStr, "invalid_grant") || strings.Contains(bodyStr, "unauthorized_client") {
return true, nil
}
return false, nil
case http.StatusNotFound:
// domain does not exists - 404 not found
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Auth0oauth
}
func (s Scanner) Description() string {
return "Auth0 is a service designed to handle authentication and authorization for users. Oauth API keys can be used to impersonate applications and other things related to Auth0's API"
}
================================================
FILE: pkg/detectors/auth0oauth/auth0oauth_integeration_test.go
================================================
//go:build detectors
// +build detectors
package auth0oauth
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAuth0oauth_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)
}
domain := testSecrets.MustGetField("AUTH0_DOMAIN")
clientId := testSecrets.MustGetField("AUTH0_CLIENT_ID")
clientSecret := testSecrets.MustGetField("AUTH0_CLIENT_SECRET")
domainUnauthorized := testSecrets.MustGetField("AUTH0_DOMAIN_UNAUTHORIZED")
clientIdUnauthorized := testSecrets.MustGetField("AUTH0_CLIENT_ID_UNAUTHORIZED")
clientSecretUnauthorized := testSecrets.MustGetField("AUTH0_CLIENT_SECRET_UNAUTHORIZED")
notFoundDomain := testSecrets.MustGetField("AUTH0_DOMAIN_NOT_FOUND")
inactiveClientSecret := testSecrets.MustGetField("AUTH0_CLIENT_SECRET_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a auth0 client id %s client secret %s domain %s", clientId, clientSecret, domain)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Auth0oauth,
Redacted: clientId,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, verified but unauthorized",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a auth0 client id %s client secret %s domain %s", clientIdUnauthorized, clientSecretUnauthorized, domainUnauthorized)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Auth0oauth,
Redacted: clientIdUnauthorized,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a auth0 client id %s client secret %s domain https://%s/oauth/token within but not valid", clientId, inactiveClientSecret, domain)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Auth0oauth,
Redacted: clientId,
Verified: false,
},
},
wantErr: 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,
},
{
name: "domain does not exists",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a auth0 client id %s client secret %s domain %s", clientId, clientSecret, notFoundDomain)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Auth0oauth,
Redacted: clientId,
Verified: false,
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Auth0oauth.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])
}
got[i].Raw = nil
if len(got[i].RawV2) == 0 {
t.Fatalf("no raw v2 secret present: \n %+v", got[i])
}
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Auth0oauth.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/auth0oauth/auth0oauth_test.go
================================================
package auth0oauth
import (
"context"
"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"
)
func TestAuth0oAuth_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
# do not share these credentials
auth0_credentials file:
auth0_clientID: kYWr_tL4eYBtqIIvKfSf2-e4T9Cw1CtwE8ufoESVBB7Hi1U
secret: rXwGtKCleBsaUfpchggQEAy_yhzWnqv4_GzJivBif85bqiJi3ZA63DAauoJ2PF27fvS-MBqIYgxH0vZaL1s5314lgPDLqHXjZsY59PSew63A_L6rySqcy5J3rFcGcpdeSQ_tTx1kCXOZY_JUy
domain: 9-KhTIdSopSaMQ2v1YxdFEJN-HNgt7Mn7E8xkfQNqd51AzSGQu2yRaFauth0.com
`,
want: []string{"kYWr_tL4eYBtqIIvKfSf2-e4T9Cw1CtwE8ufoESVBB7Hi1UrXwGtKCleBsaUfpchggQEAy_yhzWnqv4_GzJivBif85bqiJi3ZA63DAauoJ2PF27fvS-MBqIYgxH0vZaL1s5314lgPDLqHXjZsY59PSew63A_L6rySqcy5J3rFcGcpdeSQ_tTx1kCXOZY_JUy"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{auth0 rP_yIAV6HD3Oe4zr6KawRXGbq6UCWbeC1kbjQkVhqG4vcLCc2}{AQAAABAAA 1PMNVllg_WHl2OGdPLSs73Z1NHjQ85nafV2qqKbQivoqEz4RSo6MFBoNxF-XqFKjEyt6WJfZvAslDPrwY-B-MLsN13rgxRrAiFw9d8Rl1e0uC0FCNDC5EALR9kq7cs4Atz_Dv4r5YT8drkV1_T5HMjH8SJb2B-jD}{kXFuauth0.com}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"rP_yIAV6HD3Oe4zr6KawRXGbq6UCWbeC1kbjQkVhqG4vcLCc21PMNVllg_WHl2OGdPLSs73Z1NHjQ85nafV2qqKbQivoqEz4RSo6MFBoNxF-XqFKjEyt6WJfZvAslDPrwY-B-MLsN13rgxRrAiFw9d8Rl1e0uC0FCNDC5EALR9kq7cs4Atz_Dv4r5YT8drkV1_T5HMjH8SJb2B-jD"},
},
{
name: "invalid pattern",
input: `
# do not share these credentials
auth0_credentials file:
auth0_clientID: e4T9Cw1CtwE8ufoESVBB7Hi1U-e4T9Cw1CtwE8ufoESVBB7Hi1U
secret: MBqIYgxH0vZaL1s5314lgPDLqHX^ZsY59PSew63A_L6rySqcy5J3rFcGcpdeSQ_+tTx1kCXOZY_JUy-rXwGtKCleBsaUfpchggQEAy_yhzWnqv4_GzJivBif85bqiJi3ZA63DAauoJ2PF27fvS
domain: 9-KhTIdSopSaMQ2v1YxdFEJN#qd51AzSGQu2yRaFauth1.com
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/autodesk/autodesk.go
================================================
package autodesk
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"autodesk"}) + `\b([0-9A-Za-z]{32})\b`)
secretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"autodesk"}) + `\b([0-9A-Za-z]{16})\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{"autodesk"}
}
// FromData will find and optionally verify Autodesk 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
secretMatches := secretPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, secretMatch := range secretMatches {
resSecret := strings.TrimSpace(secretMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Autodesk,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resSecret),
}
if verify {
payload := strings.NewReader(fmt.Sprintf(`grant_type=client_credentials&client_id=%s&client_secret=%s`, resMatch, resSecret))
req, err := http.NewRequestWithContext(ctx, "POST", "https://developer.api.autodesk.com/authentication/v1/authenticate", payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Autodesk
}
func (s Scanner) Description() string {
return "Autodesk provides software services for design and engineering. Autodesk API keys can be used to access and modify data within Autodesk services."
}
================================================
FILE: pkg/detectors/autodesk/autodesk_integration_test.go
================================================
//go:build detectors
// +build detectors
package autodesk
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAutodesk_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
id := testSecrets.MustGetField("AUTODESK_ID")
secret := testSecrets.MustGetField("AUTODESK_SECRET")
inactiveID := testSecrets.MustGetField("AUTODESK_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a autodesk secret %s within autodesk id %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Autodesk,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a autodesk secret %s within autodesk id %s but not valid", secret, inactiveID)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Autodesk,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Autodesk.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Autodesk.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/autodesk/autodesk_test.go
================================================
package autodesk
import (
"context"
"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"
)
func TestAutoDesk_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using autodesk Key=2j8Rl67MjoMruYfyIBgGzy2pxcxIQfet
[DEBUG] Using autodesk Secret=rHfzZhsSRruLM3Fn
[INFO] Response received: 200 OK
`,
want: []string{"2j8Rl67MjoMruYfyIBgGzy2pxcxIQfetrHfzZhsSRruLM3Fn"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{autodesk 0xjHuuRZc8n0YS6MGd8e3OakAySlK27q}{autodesk AQAAABAAA 0TvJm15Ew8KADWTN}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"0xjHuuRZc8n0YS6MGd8e3OakAySlK27q0TvJm15Ew8KADWTN"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the API
[DEBUG] Using autodesk Key=2mm8Rl67MjoMruYfyIBg5#zy2pxcxIQfet
[DEBUG] Using autodesk Secret=RHGklpa
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/autoklose/autoklose.go
================================================
package autoklose
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"autoklose"}) + `\b([a-zA-Z0-9-]{32})\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{"autoklose"}
}
// FromData will find and optionally verify Autoklose 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Autoklose,
Raw: []byte(resMatch),
}
if verify {
isVerified, extraData, err := verifyMatch(ctx, client, resMatch)
s1.Verified = isVerified
s1.ExtraData = extraData
s1.SetVerificationError(err, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, map[string]string, error) {
// API Documentation: https://api.aklab.xyz/#auth-info-fd71acd1-2e41-4991-8789-3edfd258479a
url := fmt.Sprintf("https://api.autoklose.com/api/me/?api_token=%s", token)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return false, nil, err
}
req.Header.Add("Accept", "application/json")
res, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, nil, err
}
var responseBody map[string]interface{}
if err := json.Unmarshal(bodyBytes, &responseBody); err != nil {
return false, nil, err
}
if email, ok := responseBody["email"].(string); ok {
return true, map[string]string{"email": email}, nil
}
return true, nil, nil
case http.StatusUnauthorized:
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Autoklose
}
func (s Scanner) Description() string {
return "Autoklose is a sales automation tool that allows users to streamline their email outreach and follow-up processes. Autoklose API tokens can be used to access and manage campaigns, contacts, and other related data."
}
================================================
FILE: pkg/detectors/autoklose/autoklose_integration_test.go
================================================
//go:build detectors
// +build detectors
package autoklose
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAutoklose_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AUTOKLOSE")
inactiveSecret := testSecrets.MustGetField("AUTOKLOSE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a autoklose secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Autoklose,
Verified: true,
ExtraData: map[string]string{
"email": "mladen.stevanovic@vanillasoft.com",
},
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a autoklose secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Autoklose,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Autoklose.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
for i := range got {
if len(got[i].Raw) == 0 {
t.Fatal("no raw secret present")
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Autoklose.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/autoklose/autoklose_test.go
================================================
package autoklose
import (
"context"
"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"
)
func TestAutoKlose_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the autoklose API
[DEBUG] Using Key=KRXaU9GK3f9yHG1FS-mbwhsIXdW22epH
[INFO] Response received: 200 OK
`,
want: []string{"KRXaU9GK3f9yHG1FS-mbwhsIXdW22epH"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{autoklose}{AQAAABAAA Z6Q4KENlmgGJT-M-BLoup9Dmyj2YVC-I}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"Z6Q4KENlmgGJT-M-BLoup9Dmyj2YVC-I"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the autoklose API
[DEBUG] Using Key=KRXaU9GK3f[yHG1FS$]bwhsIXdW22epH
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/autopilot/autopilot.go
================================================
package autopilot
import (
"context"
regexp "github.com/wasilibs/go-re2"
"net/http"
"strings"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"autopilot"}) + `\b([0-9a-f]{32})\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{"autopilot"}
}
// FromData will find and optionally verify AutoPilot 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AutoPilot,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api2.autopilothq.com/v1/account", nil)
if err != nil {
continue
}
req.Header.Add("autopilotapikey", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AutoPilot
}
func (s Scanner) Description() string {
return "AutoPilot is a marketing automation platform. AutoPilot API keys can be used to access and manage marketing data and campaigns."
}
================================================
FILE: pkg/detectors/autopilot/autopilot_integration_test.go
================================================
//go:build detectors
// +build detectors
package autopilot
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAutoPilot_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AUTOPILOT")
inactiveSecret := testSecrets.MustGetField("AUTOPILOT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a autopilot secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AutoPilot,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a autopilot secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AutoPilot,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AutoPilot.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("AutoPilot.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/autopilot/autopilot_test.go
================================================
package autopilot
import (
"context"
"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"
)
func TestAutoPilot_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the autopilot API
[DEBUG] Using Key=0fd87cfb1ca6c38c5f1ae5be7b0e395e
[INFO] Response received: 200 OK
`,
want: []string{"0fd87cfb1ca6c38c5f1ae5be7b0e395e"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{autopilot}{AQAAABAAA 60aa8204a2b1dec8af7de45737fed7be}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"60aa8204a2b1dec8af7de45737fed7be"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the autopilot API
[DEBUG] Using Key=KRXaU9GK3f[yHG1FS$]bwhsIXdW22epH
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/avazapersonalaccesstoken/avazapersonalaccesstoken.go
================================================
package avazapersonalaccesstoken
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
// The number prefix increments for every Personal Access Token created.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"avaza"}) + `\b([0-9]+-[0-9a-f]{40})\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{"avaza"}
}
// FromData will find and optionally verify AvazaPersonalAccessToken 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AvazaPersonalAccessToken,
Raw: []byte(resMatch),
}
if verify {
isVerified, err := verifyMatch(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(err, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) {
// API Documentation: https://api.avaza.com/swagger/ui/index#!/Account/Account_Get
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.avaza.com/api/Account", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", 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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AvazaPersonalAccessToken
}
func (s Scanner) Description() string {
return "Avaza is a business management tool that offers project management, time tracking, and financial management. Avaza Personal Access Tokens can be used to access and interact with Avaza's API."
}
================================================
FILE: pkg/detectors/avazapersonalaccesstoken/avazapersonalaccesstoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package avazapersonalaccesstoken
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAvazaPersonalAccessToken_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AVAZAPERSONALACCESSTOKEN")
inactiveSecret := testSecrets.MustGetField("AVAZAPERSONALACCESSTOKEN_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a avaza secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AvazaPersonalAccessToken,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a avaza secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AvazaPersonalAccessToken,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AvazaPersonalAccessToken.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("AvazaPersonalAccessToken.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/avazapersonalaccesstoken/avazapersonalaccesstoken_test.go
================================================
package avazapersonalaccesstoken
import (
"context"
"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"
)
func TestAvazaPersonalAccessToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the avaza API
[DEBUG] Using Key=01818612883613176996369293-f113ceb9cf4fa63dc367ab4815b0e1edf890745f
[INFO] Response received: 200 OK
`,
want: []string{"01818612883613176996369293-f113ceb9cf4fa63dc367ab4815b0e1edf890745f"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{avaza}{AQAAABAAA 6605785514902-06e236581be50b798459a53fcb7609032bf813f7}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"6605785514902-06e236581be50b798459a53fcb7609032bf813f7"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the avaza API
[DEBUG] Using Key=01818612883613176996369293-fzz3ceb0mf4fp63dh367xb4815b0e1edf890745f
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/aviationstack/aviationstack.go
================================================
package aviationstack
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"aviationstack"}) + `\b([a-z0-9]{32})\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{"aviationstack"}
}
// FromData will find and optionally verify AviationStack 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AviationStack,
Raw: []byte(resMatch),
}
if verify {
isVerified, err := verifyMatch(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(err, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) {
client.Timeout = 10 * time.Second
url := fmt.Sprintf("https://api.aviationstack.com/v1/flights?access_key=%s", token)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return false, err
}
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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AviationStack
}
func (s Scanner) Description() string {
return "AviationStack is a service providing real-time flight status and aviation data. The API key can be used to access this data."
}
================================================
FILE: pkg/detectors/aviationstack/aviationstack_integration_test.go
================================================
//go:build detectors
// +build detectors
package aviationstack
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAviationStack_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AVIATIONSTACK")
inactiveSecret := testSecrets.MustGetField("AVIATIONSTACK_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aviationstack secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AviationStack,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aviationstack secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AviationStack,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AviationStack.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("AviationStack.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/aviationstack/aviationstack_test.go
================================================
package aviationstack
import (
"context"
"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"
)
func TestAviationStack_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the aviationstack API
[DEBUG] Using Key=osh0kjinsc2atoaqntoy1hdjppg54449
[INFO] Response received: 200 OK
`,
want: []string{"osh0kjinsc2atoaqntoy1hdjppg54449"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{aviationstack}{AQAAABAAA 464r3ib5xzipgd36zdzpvm09p00juu0b}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"464r3ib5xzipgd36zdzpvm09p00juu0b"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the aviationstack API
[DEBUG] Using Key=OSh0lMjinsc2atoaqnto[]1hdjppg5449
[ERROR] Response received: 400 BadRequest
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/aws/access_keys/accesskey.go
================================================
package access_keys
import (
"context"
"fmt"
"net"
"net/http"
"strings"
"time"
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/aws/smithy-go/middleware"
smithyhttp "github.com/aws/smithy-go/transport/http"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/aws"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type scanner struct {
verificationClient config.HTTPClient
skipIDs map[string]struct{}
detectors.AccountFilter
detectors.DefaultMultiPartCredentialProvider
}
func New(opts ...func(*scanner)) *scanner {
scanner := &scanner{
skipIDs: map[string]struct{}{},
}
for _, opt := range opts {
opt(scanner)
}
return scanner
}
func WithSkipIDs(skipIDs []string) func(*scanner) {
return func(s *scanner) {
ids := map[string]struct{}{}
for _, id := range skipIDs {
ids[id] = struct{}{}
}
s.skipIDs = ids
}
}
func WithAllowedAccounts(accounts []string) func(*scanner) {
return func(s *scanner) {
s.SetAllowedAccounts(accounts)
}
}
func WithDeniedAccounts(accounts []string) func(*scanner) {
return func(s *scanner) {
s.SetDeniedAccounts(accounts)
}
}
// Ensure the scanner satisfies the interface at compile time.
var _ interface {
detectors.Detector
detectors.CustomResultsCleaner
detectors.MultiPartCredentialProvider
} = (*scanner)(nil)
var (
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
// Key types are from this list https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-unique-ids
idPat = regexp.MustCompile(`\b((?:AKIA|ABIA|ACCA)[A-Z0-9]{16})\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{
"AKIA",
"ABIA",
"ACCA",
}
}
// The recommended way by AWS is to use the SDK's http client.
// https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/configure-http.html
// Note: Using default http.Client causes SignatureInvalid error in response. therefore, based on http default client implementation, we are using the same configuration.
func getDefaultBuildableClient() *awshttp.BuildableClient {
return awshttp.NewBuildableClient().
WithTimeout(common.DefaultResponseTimeout).
WithDialerOptions(func(dialer *net.Dialer) {
dialer.Timeout = 2 * time.Second
dialer.KeepAlive = 5 * time.Second
}).
WithTransportOptions(func(tr *http.Transport) {
tr.Proxy = http.ProxyFromEnvironment
tr.MaxIdleConns = 5
tr.IdleConnTimeout = 5 * time.Second
tr.TLSHandshakeTimeout = 3 * time.Second
tr.ExpectContinueTimeout = 1 * time.Second
})
}
func (s scanner) getAWSBuilableClient() config.HTTPClient {
if s.verificationClient == nil {
s.verificationClient = getDefaultBuildableClient()
}
return s.verificationClient
}
// FromData will find and optionally verify AWS secrets in a given set of bytes.
func (s scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
logger := logContext.AddLogger(ctx).Logger().WithName("aws")
dataStr := string(data)
dataStr = aws.UrlEncodedReplacer.Replace(dataStr)
// Filter & deduplicate matches.
idMatches := make(map[string]struct{})
for _, matches := range idPat.FindAllStringSubmatch(dataStr, -1) {
idMatches[matches[1]] = struct{}{}
}
secretMatches := make(map[string]struct{})
for _, matches := range aws.SecretPat.FindAllStringSubmatch(dataStr, -1) {
secretMatches[matches[1]] = struct{}{}
}
// Process matches.
for idMatch := range idMatches {
if detectors.StringShannonEntropy(idMatch) < aws.RequiredIdEntropy {
continue
}
if s.skipIDs != nil {
if _, ok := s.skipIDs[idMatch]; ok {
continue
}
}
for secretMatch := range secretMatches {
if detectors.StringShannonEntropy(secretMatch) < aws.RequiredSecretEntropy {
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AWS,
Raw: []byte(idMatch),
Redacted: idMatch,
RawV2: []byte(idMatch + ":" + secretMatch),
ExtraData: map[string]string{
"resource_type": aws.ResourceTypes[idMatch[:4]],
},
AnalysisInfo: map[string]string{
"access_key_id": idMatch,
"secret_access_key": secretMatch,
},
}
// Decode the AWS Account ID.
accountID, err := aws.GetAccountNumFromID(idMatch)
isCanary := false
if err != nil {
logger.V(3).Info("Failed to decode AWS Account ID", "err", err)
} else {
s1.ExtraData["account"] = accountID
// Check if this is a canary token
if _, ok := thinkstCanaryList[accountID]; ok {
isCanary = true
s1.ExtraData["message"] = thinkstMessage
}
if _, ok := thinkstKnockoffsCanaryList[accountID]; ok {
isCanary = true
s1.ExtraData["message"] = thinkstKnockoffsMessage
}
if isCanary {
s1.ExtraData["is_canary"] = "true"
}
}
if verify {
// Check account filtering before verification for ALL secrets (including canaries)
if accountID != "" {
if s.ShouldSkipAccount(accountID) {
var skipReason string
if s.IsInDenyList(accountID) {
skipReason = aws.VerificationErrAccountIDInDenyList
} else {
skipReason = aws.VerificationErrAccountIDNotInAllowList
}
s1.SetVerificationError(fmt.Errorf("%s", skipReason), secretMatch)
results = append(results, s1)
continue
}
}
// Perform verification based on token type
if isCanary {
// Canary verification logic
verified, arn, err := s.verifyCanary(ctx, idMatch, secretMatch)
s1.Verified = verified
if arn != "" {
s1.ExtraData["arn"] = arn
}
s1.SetVerificationError(err, secretMatch)
} else {
// Normal verification logic
isVerified, extraData, verificationErr := s.verifyMatch(ctx, idMatch, secretMatch, len(secretMatches) > 1)
s1.Verified = isVerified
// Log if the calculated ID does not match the ID value from verification.
// Should only be edge cases at most.
if accountID != "" && extraData["account"] != "" && extraData["account"] != s1.ExtraData["account"] {
logger.V(2).Info("Calculated account ID does not match actual account ID", "calculated", accountID, "actual", extraData["account"])
}
// Append the extraData to the existing ExtraData map.
for k, v := range extraData {
s1.ExtraData[k] = v
}
s1.SetVerificationError(verificationErr, secretMatch)
}
}
if !s1.Verified && aws.FalsePositiveSecretPat.MatchString(secretMatch) {
// Unverified results that look like hashes are probably not secrets
continue
}
results = append(results, s1)
// If we've found a verified match with this ID, we don't need to look for any more. So move on to the next ID.
if s1.Verified {
delete(secretMatches, secretMatch)
break
}
}
}
return results, nil
}
func (s scanner) ShouldCleanResultsIrrespectiveOfConfiguration() bool {
return true
}
const (
method = "GET"
service = "sts"
host = "sts.amazonaws.com"
region = "us-east-1"
endpoint = "https://sts.amazonaws.com"
)
func (s scanner) verifyMatch(ctx context.Context, resIDMatch, resSecretMatch string, retryOn403 bool) (bool, map[string]string, error) {
// Prep AWS Creds for STS
cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion(region),
config.WithHTTPClient(s.getAWSBuilableClient()),
config.WithCredentialsProvider(
credentials.NewStaticCredentialsProvider(resIDMatch, resSecretMatch, ""),
),
)
if err != nil {
return false, nil, err
}
// Create STS client
stsClient := sts.NewFromConfig(cfg, func(o *sts.Options) {
o.APIOptions = append(o.APIOptions, replaceUserAgentMiddleware)
})
// Make the GetCallerIdentity API call
resp, err := stsClient.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
if err != nil {
// Experimentation has indicated that if you make multiple GetCallerIdentity requests within five seconds that
// share a key ID but are signed with different secrets the second one will be rejected with a 403 that
// carries a SignatureDoesNotMatch code in its body. This happens even if the second ID-secret pair is
// valid. Since this is exactly our access pattern, we need to work around it.
//
// Fortunately, experimentation has also revealed a workaround: simply resubmit the second request. The
// response to the resubmission will be as expected.
//
// We are clearly deep in the guts of AWS implementation details here, so this all might change with no
// notice. If you're here because something in this detector broke, you have my condolences.
if strings.Contains(err.Error(), "StatusCode: 403") {
if retryOn403 {
return s.verifyMatch(ctx, resIDMatch, resSecretMatch, false)
}
return false, nil, nil
} else if strings.Contains(err.Error(), "InvalidClientTokenId") {
return false, nil, nil
}
return false, nil, fmt.Errorf("request returned unexpected error: %w", err)
}
extraData := map[string]string{
"rotation_guide": "https://howtorotate.com/docs/tutorials/aws/",
"account": *resp.Account,
"user_id": *resp.UserId,
"arn": *resp.Arn,
}
return true, extraData, nil
}
func (s scanner) CleanResults(results []detectors.Result) []detectors.Result {
return aws.CleanResults(results)
}
func (s scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AWS
}
func (s scanner) Description() string {
return "AWS (Amazon Web Services) is a comprehensive cloud computing platform offering a wide range of on-demand services like computing power, storage, databases. API keys for AWS can have varying amount of access to these services depending on the IAM policy attached."
}
// Adds a custom Build middleware to the stack to replace the User-Agent header of the final request
// https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/middleware.html
func replaceUserAgentMiddleware(stack *middleware.Stack) error {
return stack.Build.Add(
middleware.BuildMiddlewareFunc(
"ReplaceUserAgent",
func(ctx context.Context, in middleware.BuildInput, next middleware.BuildHandler) (
out middleware.BuildOutput, metadata middleware.Metadata, err error,
) {
req, ok := in.Request.(*smithyhttp.Request)
if !ok {
return next.HandleBuild(ctx, in)
}
req.Header.Set("User-Agent", common.UserAgent())
return next.HandleBuild(ctx, in)
},
),
middleware.After,
)
}
================================================
FILE: pkg/detectors/aws/access_keys/accesskey_integration_test.go
================================================
//go:build detectors
// +build detectors
package access_keys
import (
"context"
"fmt"
"sort"
"testing"
"time"
"github.com/brianvoe/gofakeit/v7"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
const canaryAccessKeyID = "AKIASP2TPHJSQH3FJRUX"
var unverifiedSecretClient = common.ConstantResponseHttpClient(403, `{"Error": {"Code": "InvalidClientTokenId"} }`)
// Our AWS detector interacts with AWS in an (expectedly) uncommon way that triggers some odd AWS behavior. (This odd
// behavior doesn't affect "normal" AWS use, so it's not really "broken" - it's just something that we have to work
// around.) The AWS detector code has a long comment explaining this in more detail, but the basic issue is that AWS STS
// is stateful, so the behavior of these tests can vary depending on which of them you run, and in which order. This
// particular test (TestAWS_FromChunk_InvalidValidReuseIDSequence) duplicates some logic in the "big" test table in the
// other test in this file, but extracting it in this way as well makes it fail more consistently when it's supposed to
// fail, which is why it's extracted.
func TestAWS_FromChunk_InvalidValidReuseIDSequence(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AWS")
id := testSecrets.MustGetField("AWS_ID")
inactiveSecret := testSecrets.MustGetField("AWS_INACTIVE")
d := scanner{}
ignoreOpts := []cmp.Option{cmpopts.IgnoreFields(detectors.Result{}, "RawV2", "Raw", "verificationError")}
got, err := d.FromData(ctx, true, []byte(fmt.Sprintf("aws %s %s", id, inactiveSecret)))
if assert.NoError(t, err) {
want := []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AWS,
Verified: false,
Redacted: "AKIAZAVB57H55F3T4BKH",
ExtraData: map[string]string{
"resource_type": "Access key",
"account": "619888638459",
},
},
}
if diff := cmp.Diff(got, want, ignoreOpts...); diff != "" {
t.Errorf("AWS.FromData() (valid ID, invalid secret) diff: (-got +want)\n%s", diff)
}
}
got, err = d.FromData(ctx, true, []byte(fmt.Sprintf("aws %s %s", id, secret)))
if assert.NoError(t, err) {
want := []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AWS,
Verified: true,
Redacted: "AKIAZAVB57H55F3T4BKH",
ExtraData: map[string]string{
"resource_type": "Access key",
"account": "619888638459",
"arn": "arn:aws:iam::619888638459:user/trufflehog-aws-detector-tester",
"rotation_guide": "https://howtorotate.com/docs/tutorials/aws/",
"user_id": "AIDAZAVB57H5V3Q4ACRGM",
},
},
}
if diff := cmp.Diff(got, want, ignoreOpts...); diff != "" {
t.Errorf("AWS.FromData() (valid secret after invalid secret using same ID) diff: (-got +want)\n%s", diff)
}
}
}
func TestAWS_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AWS")
id := testSecrets.MustGetField("AWS_ID")
inactiveSecret := testSecrets.MustGetField("AWS_INACTIVE")
inactiveID := id[:len(id)-3] + "XYZ"
hash := gofakeit.Password(true, true, true, false, false, 10)
type args struct {
ctx context.Context
data []byte
verify bool
}
tests := []struct {
name string
s scanner
args args
want []detectors.Result
wantErr bool
wantVerificationError bool
}{
{
name: "found, verified",
s: scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aws secret %s within aws %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AWS,
Verified: true,
Redacted: "AKIAZAVB57H55F3T4BKH",
ExtraData: map[string]string{
"resource_type": "Access key",
"account": "619888638459",
"arn": "arn:aws:iam::619888638459:user/trufflehog-aws-detector-tester",
"rotation_guide": "https://howtorotate.com/docs/tutorials/aws/",
"user_id": "AIDAZAVB57H5V3Q4ACRGM",
},
},
},
wantErr: false,
},
{
name: "found, unverified",
s: scanner{verificationClient: unverifiedSecretClient},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aws secret %s within aws %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AWS,
Verified: false,
Redacted: "AKIAZAVB57H55F3T4BKH",
ExtraData: map[string]string{
"resource_type": "Access key",
"account": "619888638459",
},
},
},
wantErr: 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,
},
{
name: "found two, one included for every ID found",
s: scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("The verified ID is %s with a secret of %s, but the unverified ID is %s and this is the secret %s", id, secret, inactiveID, inactiveSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AWS,
Verified: false,
Redacted: "AKIAZAVB57H55F3T4XYZ",
ExtraData: map[string]string{
"resource_type": "Access key",
"account": "619888638459",
},
},
{
DetectorType: detectorspb.DetectorType_AWS,
Verified: true,
Redacted: "AKIAZAVB57H55F3T4BKH",
ExtraData: map[string]string{
"resource_type": "Access key",
"account": "619888638459",
"arn": "arn:aws:iam::619888638459:user/trufflehog-aws-detector-tester",
"rotation_guide": "https://howtorotate.com/docs/tutorials/aws/",
"user_id": "AIDAZAVB57H5V3Q4ACRGM",
},
},
},
wantErr: false,
},
{
name: "not found, because unverified secret was a hash",
s: scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aws secret %s within aws %s but not valid", hash, id)), // The secret would satisfy the regex but be filtered out after not passing validation.
verify: true,
},
want: nil,
wantErr: false,
},
{
name: "found two, returned both because the active secret for one paired with the inactive ID, despite the hash",
s: scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("The verified ID is %s with a secret of %s, but the unverified ID is %s and the secret is this hash %s", id, secret, inactiveID, hash)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AWS,
Verified: true,
Redacted: "AKIAZAVB57H55F3T4BKH",
ExtraData: map[string]string{
"resource_type": "Access key",
"account": "619888638459",
"arn": "arn:aws:iam::619888638459:user/trufflehog-aws-detector-tester",
"rotation_guide": "https://howtorotate.com/docs/tutorials/aws/",
"user_id": "AIDAZAVB57H5V3Q4ACRGM",
},
},
},
wantErr: false,
},
{
name: "found, unverified, with leading +",
s: scanner{
verificationClient: unverifiedSecretClient,
},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aws secret %s within aws %s but not valid", "+HaNv9cTwheDKGJaws/+BMF2GgybQgBWdhcOOdfF", id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AWS,
Verified: false,
Redacted: "AKIAZAVB57H55F3T4BKH",
ExtraData: map[string]string{
"resource_type": "Access key",
"account": "619888638459",
},
},
},
wantErr: false,
},
{
name: "skipped",
s: scanner{
skipIDs: map[string]struct{}{
"AKIAZAVB57H55F3T4BKH": {},
},
},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aws secret %s within aws %s but not valid", "+HaNv9cTwheDKGJaws/+BMF2GgybQgBWdhcOOdfF", id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
wantErr: false,
},
{
name: "found, would be verified if not for http timeout",
s: scanner{
verificationClient: common.SaneHttpClientTimeOut(1 * time.Microsecond),
},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aws secret %s within aws %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AWS,
Verified: false,
Redacted: "AKIAZAVB57H55F3T4BKH",
ExtraData: map[string]string{
"resource_type": "Access key",
"account": "619888638459",
},
},
},
wantErr: false,
wantVerificationError: true,
},
{
name: "found, unverified due to unexpected http response status",
s: scanner{
verificationClient: common.ConstantResponseHttpClient(500, "internal server error"),
},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aws secret %s within aws %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AWS,
Verified: false,
Redacted: "AKIAZAVB57H55F3T4BKH",
ExtraData: map[string]string{
"resource_type": "Access key",
"account": "619888638459",
},
},
},
wantErr: false,
wantVerificationError: true,
},
{
name: "found, unverified due to invalid aws_secret with valid canary access_key_id",
s: scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aws secret %s within aws %s", inactiveSecret, canaryAccessKeyID)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AWS,
Verified: false,
Redacted: canaryAccessKeyID,
ExtraData: map[string]string{
"resource_type": "Access key",
"account": "171436882533",
"is_canary": "true",
"message": "This is an AWS canary token generated at canarytokens.org, and was not set off; learn more here: https://trufflesecurity.com/canaries",
},
},
},
wantErr: false,
wantVerificationError: false,
},
{
name: "found, valid canary token with no verification",
s: scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aws secret %s within aws %s", secret, canaryAccessKeyID)),
verify: false,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AWS,
Verified: false,
Redacted: canaryAccessKeyID,
ExtraData: map[string]string{
"resource_type": "Access key",
"account": "171436882533",
"is_canary": "true",
"message": "This is an AWS canary token generated at canarytokens.org, and was not set off; learn more here: https://trufflesecurity.com/canaries",
},
},
},
wantErr: false,
wantVerificationError: false,
},
{
name: "verified secret checked directly after unverified secret with same key id",
s: scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("%s\n%s\n%s", inactiveSecret, id, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AWS,
Verified: false,
Redacted: "AKIAZAVB57H55F3T4BKH",
ExtraData: map[string]string{
"resource_type": "Access key",
"account": "619888638459",
},
},
{
DetectorType: detectorspb.DetectorType_AWS,
Verified: true,
Redacted: "AKIAZAVB57H55F3T4BKH",
ExtraData: map[string]string{
"resource_type": "Access key",
"account": "619888638459",
"arn": "arn:aws:iam::619888638459:user/trufflehog-aws-detector-tester",
"rotation_guide": "https://howtorotate.com/docs/tutorials/aws/",
"user_id": "AIDAZAVB57H5V3Q4ACRGM",
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := tt.s
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AWS.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.wantVerificationError {
t.Fatalf("wantVerificationError %v, verification error = %v", tt.wantVerificationError, got[i].VerificationError())
}
}
ignoreOpts := []cmp.Option{
cmpopts.IgnoreFields(detectors.Result{}, "RawV2", "Raw", "verificationError"),
cmpopts.SortSlices(func(x, y detectors.Result) bool {
return x.Redacted < y.Redacted
}),
}
sortResults(tt.want)
if diff := cmp.Diff(got, tt.want, ignoreOpts...); diff != "" {
t.Errorf("AWS.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
}
})
}
}
// Helper function to sort results due to the order of the redacted
func sortResults(results []detectors.Result) {
sort.SliceStable(results, func(i, j int) bool {
return results[i].Redacted < results[j].Redacted
})
}
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)
}
}
})
}
}
================================================
FILE: pkg/detectors/aws/access_keys/accesskey_test.go
================================================
package access_keys
import (
"context"
"fmt"
"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"
)
func TestAWS_Pattern(t *testing.T) {
d := scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
aws credentials{
id: ABIAS9L8MS5IPHTZPPUQ
secret: .v2QPKHl7LcdVYsjaR4LgQiZ1zw3MAnMyiondXC63;
}
`,
want: []string{"ABIAS9L8MS5IPHTZPPUQ:v2QPKHl7LcdVYsjaR4LgQiZ1zw3MAnMyiondXC63"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{AKIAWGXZ9OPDOWUJMZGI}{AQAAABAAA .v2QPKHl7LcdVYsjaR4LgQiZ1zw3MAnMyiondXC63;}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"AKIAWGXZ9OPDOWUJMZGI:v2QPKHl7LcdVYsjaR4LgQiZ1zw3MAnMyiondXC63"},
},
{
name: "invalid pattern",
input: `
aws credentials{
id: AKIAs9L8MS5iPHTZPPUQ
secret: $YenOG.PKHl7LcdVYsjaR4LgQiZ1zw3MAnMyiondXC63;
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
func TestAWS_WithAllowedAccounts(t *testing.T) {
accounts := []string{"123456789012", "999888777666"}
s := New(WithAllowedAccounts(accounts))
// Test that allowed accounts are properly configured
shouldSkip := s.ShouldSkipAccount("123456789012")
require.False(t, shouldSkip)
require.True(t, s.IsInAllowList("123456789012"))
// Test that non-allowed accounts are skipped
shouldSkip = s.ShouldSkipAccount("111222333444")
require.True(t, shouldSkip)
require.False(t, s.IsInAllowList("111222333444"))
}
func TestAWS_WithDeniedAccounts(t *testing.T) {
accounts := []string{"123456789012", "999888777666"}
s := New(WithDeniedAccounts(accounts))
// Test that denied accounts are properly skipped
shouldSkip := s.ShouldSkipAccount("123456789012")
require.True(t, shouldSkip)
require.True(t, s.IsInDenyList("123456789012"))
// Test that non-denied accounts are not skipped
shouldSkip = s.ShouldSkipAccount("111222333444")
require.False(t, shouldSkip)
require.False(t, s.IsInDenyList("111222333444"))
}
func TestAWS_CanaryTokenFiltering(t *testing.T) {
// Using known canary token from integration tests
canaryAccessKeyID := "AKIASP2TPHJSQH3FJRUX" // Account ID: 171436882533
canarySecret := "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
testData := []byte(fmt.Sprintf("%s:%s", canaryAccessKeyID, canarySecret))
t.Run("debug canary detection", func(t *testing.T) {
// First, let's test basic canary detection without verification
s := New()
results, err := s.FromData(context.Background(), false, testData) // verify = false
require.NoError(t, err)
require.Len(t, results, 1)
result := results[0]
t.Logf("Result without verification - Verified: %v, Account: %s, IsCanary: %s, Message: %s",
result.Verified, result.ExtraData["account"], result.ExtraData["is_canary"], result.ExtraData["message"])
// Should detect as canary but not verify (since verify=false)
require.False(t, result.Verified)
require.Equal(t, "171436882533", result.ExtraData["account"])
require.Equal(t, "true", result.ExtraData["is_canary"])
require.Contains(t, result.ExtraData["message"], "canarytokens.org")
})
t.Run("canary token with allow list - account not allowed", func(t *testing.T) {
// Configure scanner with allow list that excludes the canary account
s := New(WithAllowedAccounts([]string{"123456789012", "999888777666"}))
results, err := s.FromData(context.Background(), true, testData)
require.NoError(t, err)
require.Len(t, results, 1)
result := results[0]
// Should detect the canary token but not verify it due to filtering
require.False(t, result.Verified)
require.NotNil(t, result.VerificationError())
require.Contains(t, result.VerificationError().Error(), "not in the allow list")
require.Equal(t, "171436882533", result.ExtraData["account"])
require.Equal(t, "true", result.ExtraData["is_canary"])
})
t.Run("canary token with deny list - account denied", func(t *testing.T) {
// Configure scanner with deny list that includes the canary account
s := New(WithDeniedAccounts([]string{"171436882533", "123456789012"}))
results, err := s.FromData(context.Background(), true, testData)
require.NoError(t, err)
require.Len(t, results, 1)
result := results[0]
// Should detect the canary token but not verify it due to filtering
require.False(t, result.Verified)
require.NotNil(t, result.VerificationError())
require.Contains(t, result.VerificationError().Error(), "in the deny list")
require.Equal(t, "171436882533", result.ExtraData["account"])
require.Equal(t, "true", result.ExtraData["is_canary"])
})
t.Run("precedence test - deny list takes precedence over allow list", func(t *testing.T) {
// Configure scanner where canary account is in both allow and deny lists
s := New(
WithAllowedAccounts([]string{"171436882533", "123456789012"}),
WithDeniedAccounts([]string{"171436882533"}),
)
results, err := s.FromData(context.Background(), true, testData)
require.NoError(t, err)
require.Len(t, results, 1)
result := results[0]
// Should detect the canary token but not verify it since deny takes precedence
require.False(t, result.Verified)
require.NotNil(t, result.VerificationError())
require.Contains(t, result.VerificationError().Error(), "in the deny list")
require.Equal(t, "171436882533", result.ExtraData["account"])
require.Equal(t, "true", result.ExtraData["is_canary"])
})
}
================================================
FILE: pkg/detectors/aws/access_keys/canary.go
================================================
package access_keys
import (
"context"
"strings"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/sns"
)
const thinkstMessage = "This is an AWS canary token generated at canarytokens.org."
const thinkstKnockoffsMessage = "This is an off brand AWS Canary inspired by canarytokens.org."
var (
thinkstCanaryList = map[string]struct{}{
"052310077262": {},
"171436882533": {},
"534261010715": {},
"595918472158": {},
"717712589309": {},
"819147034852": {},
"992382622183": {},
"730335385048": {},
"266735846894": {},
"893192397702": {},
}
thinkstKnockoffsCanaryList = map[string]struct{}{
"044858866125": {},
"251535659677": {},
"344043088457": {},
"351906852752": {},
"390477818340": {},
"426127672474": {},
"427150556519": {},
"439872796651": {},
"445142720921": {},
"465867158099": {},
"637958123769": {},
"693412236332": {},
"732624840810": {},
"735421457923": {},
"959235150393": {},
"982842642351": {},
}
)
func (s scanner) verifyCanary(ctx context.Context, resIDMatch, resSecretMatch string) (bool, string, error) {
// Prep AWS Creds for SNS
cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion(region),
config.WithHTTPClient(s.getAWSBuilableClient()),
config.WithCredentialsProvider(
credentials.NewStaticCredentialsProvider(resIDMatch, resSecretMatch, ""),
),
)
if err != nil {
return false, "", err
}
svc := sns.NewFromConfig(cfg, func(o *sns.Options) {
o.APIOptions = append(o.APIOptions, replaceUserAgentMiddleware)
})
// Prep vars and Publish to SNS
_, err = svc.Publish(ctx, &sns.PublishInput{
Message: aws.String("foo"),
PhoneNumber: aws.String("1"),
})
if strings.Contains(err.Error(), "not authorized to perform") {
arn := strings.Split(err.Error(), "User: ")[1]
arn = strings.Split(arn, " is not authorized to perform: ")[0]
return true, arn, nil
} else if strings.Contains(err.Error(), "does not match the signature you provided") {
return false, "", nil
} else if strings.Contains(err.Error(), "status code: 403") || strings.Contains(err.Error(), "InvalidClientTokenId") {
return false, "", nil
} else {
return false, "", err
}
}
================================================
FILE: pkg/detectors/aws/common.go
================================================
package aws
import regexp "github.com/wasilibs/go-re2"
const (
RequiredIdEntropy = 3.0
RequiredSecretEntropy = 4.25
)
// Verification error messages
const (
VerificationErrAccountIDInDenyList = "Account ID is in the deny list for verification"
VerificationErrAccountIDNotInAllowList = "Account ID is not in the allow list for verification"
)
var SecretPat = regexp.MustCompile(`(?:[^A-Za-z0-9+/]|\A)([A-Za-z0-9+/]{40})(?:[^A-Za-z0-9+/]|\z)`)
type IdentityResponse struct {
GetCallerIdentityResponse struct {
GetCallerIdentityResult struct {
Account string `json:"Account"`
Arn string `json:"Arn"`
UserID string `json:"UserId"`
} `json:"GetCallerIdentityResult"`
ResponseMetadata struct {
RequestID string `json:"RequestId"`
} `json:"ResponseMetadata"`
} `json:"GetCallerIdentityResponse"`
}
type Error struct {
Code string `json:"Code"`
Message string `json:"Message"`
}
type ErrorResponseBody struct {
Error Error `json:"Error"`
}
================================================
FILE: pkg/detectors/aws/session_keys/sessionkey.go
================================================
package session_keys
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/aws"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type scanner struct {
*detectors.CustomMultiPartCredentialProvider
verificationClient *http.Client
skipIDs map[string]struct{}
detectors.AccountFilter
}
func New(opts ...func(*scanner)) *scanner {
scanner := &scanner{
skipIDs: map[string]struct{}{},
}
for _, opt := range opts {
opt(scanner)
}
scanner.CustomMultiPartCredentialProvider = detectors.NewCustomMultiPartCredentialProvider(2048)
return scanner
}
func WithSkipIDs(skipIDs []string) func(*scanner) {
return func(s *scanner) {
ids := map[string]struct{}{}
for _, id := range skipIDs {
ids[id] = struct{}{}
}
s.skipIDs = ids
}
}
func WithAllowedAccounts(accounts []string) func(*scanner) {
return func(s *scanner) {
s.SetAllowedAccounts(accounts)
}
}
func WithDeniedAccounts(accounts []string) func(*scanner) {
return func(s *scanner) {
s.SetDeniedAccounts(accounts)
}
}
// Ensure the scanner satisfies the interface at compile time.
var _ interface {
detectors.Detector
detectors.CustomResultsCleaner
} = (*scanner)(nil)
var (
defaultVerificationClient = common.SaneHttpClient()
// Key types are from this list https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-unique-ids
idPat = regexp.MustCompile(`\b((?:ASIA)[A-Z0-9]{16})\b`)
sessionPat = regexp.MustCompile(`(?:[^A-Za-z0-9+/]|\A)([a-zA-Z0-9+/]{100,}={0,3})(?:[^A-Za-z0-9+/=]|\z)`)
)
// 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{"ASIA"}
}
// FromData will find and optionally verify AWS secrets in a given set of bytes.
func (s scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
logger := logContext.AddLogger(ctx).Logger().WithName("awssessionkey")
dataStr := string(data)
dataStr = aws.UrlEncodedReplacer.Replace(dataStr)
// Filter & deduplicate matches.
idMatches := make(map[string]struct{})
for _, matches := range idPat.FindAllStringSubmatch(dataStr, -1) {
idMatches[matches[1]] = struct{}{}
}
secretMatches := make(map[string]struct{})
for _, matches := range aws.SecretPat.FindAllStringSubmatch(dataStr, -1) {
secretMatches[matches[1]] = struct{}{}
}
sessionMatches := make(map[string]struct{})
for _, matches := range sessionPat.FindAllStringSubmatch(dataStr, -1) {
sessionMatches[matches[1]] = struct{}{}
}
// Process matches.
for idMatch := range idMatches {
if detectors.StringShannonEntropy(idMatch) < aws.RequiredIdEntropy {
continue
}
if s.skipIDs != nil {
if _, ok := s.skipIDs[idMatch]; ok {
continue
}
}
for secretMatch := range secretMatches {
if detectors.StringShannonEntropy(secretMatch) < aws.RequiredSecretEntropy {
continue
}
for sessionMatch := range sessionMatches {
if detectors.StringShannonEntropy(sessionMatch) < 4.5 {
continue
}
if !checkSessionToken(sessionMatch, secretMatch) {
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AWSSessionKey,
Raw: []byte(idMatch),
RawV2: []byte(fmt.Sprintf("%s:%s:%s", idMatch, secretMatch, sessionMatch)),
Redacted: idMatch,
ExtraData: make(map[string]string),
}
if verify {
// If we haven't already found an AWS Account ID for this ID (via API), calculate one for filtering.
var accountIDForFiltering string
if accountID, err := aws.GetAccountNumFromID(idMatch); err == nil {
accountIDForFiltering = accountID
}
// Check account filtering before verification
if accountIDForFiltering != "" {
if s.ShouldSkipAccount(accountIDForFiltering) {
var skipReason string
if s.IsInDenyList(accountIDForFiltering) {
skipReason = aws.VerificationErrAccountIDInDenyList
} else {
skipReason = aws.VerificationErrAccountIDNotInAllowList
}
s1.SetVerificationError(fmt.Errorf("%s", skipReason), secretMatch)
// If we haven't already found an AWS Account ID for this ID (via API), calculate one.
if _, ok := s1.ExtraData["account"]; !ok {
if accountID, err := aws.GetAccountNumFromID(idMatch); err != nil {
logger.V(3).Info("Failed to decode AWS Account ID", "err", err)
} else {
s1.ExtraData["account"] = accountID
}
}
results = append(results, s1)
continue
}
}
isVerified, extraData, verificationErr := s.verifyMatch(ctx, idMatch, secretMatch, sessionMatch, true)
s1.Verified = isVerified
if extraData != nil {
s1.ExtraData = extraData
}
s1.SetVerificationError(verificationErr, secretMatch)
}
if !s1.Verified && aws.FalsePositiveSecretPat.MatchString(secretMatch) {
// Unverified results that look like hashes are probably not secrets
continue
}
// If we haven't already found an AWS Account ID for this ID (via API), calculate one.
if _, ok := s1.ExtraData["account"]; !ok {
if accountID, err := aws.GetAccountNumFromID(idMatch); err != nil {
logger.V(3).Info("Failed to decode AWS Account ID", "err", err)
} else {
s1.ExtraData["account"] = accountID
}
}
results = append(results, s1)
// If we've found a verified match with this ID, we don't need to look for any more. So move on to the next ID.
if s1.Verified {
delete(sessionMatches, secretMatch)
delete(sessionMatches, sessionMatch)
break
}
}
}
}
return results, nil
}
func (s scanner) ShouldCleanResultsIrrespectiveOfConfiguration() bool {
return true
}
const (
method = "GET"
service = "sts"
host = "sts.amazonaws.com"
region = "us-east-1"
endpoint = "https://sts.amazonaws.com"
)
func (s scanner) verifyMatch(ctx context.Context, resIDMatch, resSecretMatch string, resSessionMatch string, retryOn403 bool) (bool, map[string]string, error) {
// REQUEST VALUES.
now := time.Now().UTC()
datestamp := now.Format("20060102")
amzDate := now.Format("20060102T150405Z")
req, err := http.NewRequestWithContext(ctx, method, endpoint, nil)
if err != nil {
return false, nil, err
}
req.Header.Set("Accept", "application/json")
// TASK 1: CREATE A CANONICAL REQUEST.
// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
canonicalURI := "/"
canonicalHeaders := "host:" + host + "\n" + "x-amz-date:" + amzDate + "\n" + "x-amz-security-token:" + resSessionMatch + "\n"
signedHeaders := "host;x-amz-date;x-amz-security-token"
algorithm := "AWS4-HMAC-SHA256"
credentialScope := fmt.Sprintf("%s/%s/%s/aws4_request", datestamp, region, service)
params := req.URL.Query()
params.Add("Action", "GetCallerIdentity")
params.Add("Version", "2011-06-15")
canonicalQuerystring := params.Encode()
payloadHash := aws.GetHash("") // empty payload
canonicalRequest := method + "\n" + canonicalURI + "\n" + canonicalQuerystring + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + payloadHash
// TASK 2: CREATE THE STRING TO SIGN.
stringToSign := algorithm + "\n" + amzDate + "\n" + credentialScope + "\n" + aws.GetHash(canonicalRequest)
// TASK 3: CALCULATE THE SIGNATURE.
// https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
hash := aws.GetHMAC([]byte(fmt.Sprintf("AWS4%s", resSecretMatch)), []byte(datestamp))
hash = aws.GetHMAC(hash, []byte(region))
hash = aws.GetHMAC(hash, []byte(service))
hash = aws.GetHMAC(hash, []byte("aws4_request"))
signature2 := aws.GetHMAC(hash, []byte(stringToSign)) // Get Signature HMAC SHA256
signature := hex.EncodeToString(signature2)
// TASK 4: ADD SIGNING INFORMATION TO THE REQUEST.
authorizationHeader := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
algorithm, resIDMatch, credentialScope, signedHeaders, signature)
req.Header.Add("Authorization", authorizationHeader)
req.Header.Add("x-amz-date", amzDate)
req.Header.Add("x-amz-security-token", resSessionMatch)
req.URL.RawQuery = params.Encode()
client := s.verificationClient
if client == nil {
client = defaultVerificationClient
}
res, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
if res.StatusCode >= 200 && res.StatusCode < 300 {
identityInfo := aws.IdentityResponse{}
if err := json.NewDecoder(res.Body).Decode(&identityInfo); err != nil {
return false, nil, err
}
extraData := map[string]string{
"rotation_guide": "https://howtorotate.com/docs/tutorials/aws/",
"account": identityInfo.GetCallerIdentityResponse.GetCallerIdentityResult.Account,
"user_id": identityInfo.GetCallerIdentityResponse.GetCallerIdentityResult.UserID,
"arn": identityInfo.GetCallerIdentityResponse.GetCallerIdentityResult.Arn,
}
return true, extraData, nil
} else if res.StatusCode == 403 {
// Experimentation has indicated that if you make two GetCallerIdentity requests within five seconds that
// share a key ID but are signed with different secrets the second one will be rejected with a 403 that
// carries a SignatureDoesNotMatch code in its body. This happens even if the second ID-secret pair is
// valid. Since this is exactly our access pattern, we need to work around it.
//
// Fortunately, experimentation has also revealed a workaround: simply resubmit the second request. The
// response to the resubmission will be as expected. But there's a caveat: You can't have closed the body of
// the response to the original second request, or read to its end, or the resubmission will also yield a
// SignatureDoesNotMatch. For this reason, we have to re-request all 403s. We can't re-request only
// SignatureDoesNotMatch responses, because we can only tell whether a given 403 is a SignatureDoesNotMatch
// after decoding its response body, which requires reading the entire response body, which disables the
// workaround.
//
// We are clearly deep in the guts of AWS implementation details here, so this all might change with no
// notice. If you're here because something in this detector broke, you have my condolences.
if retryOn403 {
return s.verifyMatch(ctx, resIDMatch, resSecretMatch, resSessionMatch, false)
}
var body aws.ErrorResponseBody
if err = json.NewDecoder(res.Body).Decode(&body); err != nil {
return false, nil, fmt.Errorf("couldn't parse the sts response body (%v)", err)
}
// All instances of the code I've seen in the wild are PascalCased but this check is
// case-insensitive out of an abundance of caution
if strings.EqualFold(body.Error.Code, "InvalidClientTokenId") {
return false, nil, nil
} else if strings.EqualFold(body.Error.Code, "ExpiredToken") {
// ExpiredToken: The security token included in the request is expired
return false, nil, nil
}
return false, nil, fmt.Errorf("request to %v returned status %d with an unexpected reason (%s: %s)", res.Request.URL, res.StatusCode, body.Error.Code, body.Error.Message)
} else {
return false, nil, fmt.Errorf("request to %v returned unexpected status %d", res.Request.URL, res.StatusCode)
}
}
func (s scanner) CleanResults(results []detectors.Result) []detectors.Result {
return aws.CleanResults(results)
}
// Reference: https://nitter.poast.org/TalBeerySec/status/1816449053841838223#m
func checkSessionToken(sessionToken string, secret string) bool {
if !(strings.Contains(sessionToken, "YXdz") || strings.Contains(sessionToken, "Jb3JpZ2luX2Vj")) ||
strings.Contains(sessionToken, secret) {
// Handle error if the sessionToken is not a valid base64 string
return false
}
return true
}
func (s scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AWSSessionKey
}
func (s scanner) Description() string {
return "AWS (Amazon Web Services) is a comprehensive cloud computing platform offering a wide range of on-demand services like computing power, storage, databases. API keys for AWS can have varying amount of access to these services depending on the IAM policy attached. AWS Session Tokens are short-lived keys."
}
================================================
FILE: pkg/detectors/aws/session_keys/sessionkeys_test.go
================================================
package session_keys
import (
"context"
"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"
)
func TestAWSSessionKey_Pattern(t *testing.T) {
d := New()
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
aws credentials{
id: ASIABBKK02W42Q3IPSPG
secret: fkhIiUwQY32Zu9e4a86g9r3WpTzfE1aXljVcgn8O
session: aSqfp/GTZbJP+tXPNCZ9GoveoM0vgxtlYXdzPQ2uYNMPPgUkt0VT7SoTLasAo7iVqWWREOUC6DEenlcgDEKyzIEgQW5Ju/b9K/Z176uD2HJYCfq/lyowHtt5PvJi7LRuf/urSorGbTcqNUvPi42YP1Ps/4F6He9hQA1io3EAGBC3ICGHXWf2IlvFoTNUyPTqhjnPEKMWZ42jblqNAdD7hLpzNXmmGhdLCjy99XK8+gjHdZHkOeD/FIjRPRZ7Jl0tdwdqFEwzRVCzL2uelMVMd3UaZ+d4I4Kf+J464piO//jxx48Fs/mG3zr5ba9m2S+6gvUZJq4j+0uJ+jf6cG/x2G9XSybqYQRwvxfNquKB4TcKiGVH5+ZbJT4ASkARadwoSPMGfvMPje+X2zAziSzXfsxYfIQKf6iJ9p7VavlDGi+Acr4kwFXW5IfQs4uGk6AVQFsoZK3o1hhLOkuOwWQEWhDQGNLXwJbFqXfELOnUQvM0Z5NUm46bjAAi4g+X9gLPNR/KjzXuuTTaWYrQEjXLb7PxS0sIttAb1w+sTXXtc1kDIsABC6KcsyGlEwji5sLkbkUa=
}
`,
want: []string{"ASIABBKK02W42Q3IPSPG:fkhIiUwQY32Zu9e4a86g9r3WpTzfE1aXljVcgn8O:aSqfp/GTZbJP+tXPNCZ9GoveoM0vgxtlYXdzPQ2uYNMPPgUkt0VT7SoTLasAo7iVqWWREOUC6DEenlcgDEKyzIEgQW5Ju/b9K/Z176uD2HJYCfq/lyowHtt5PvJi7LRuf/urSorGbTcqNUvPi42YP1Ps/4F6He9hQA1io3EAGBC3ICGHXWf2IlvFoTNUyPTqhjnPEKMWZ42jblqNAdD7hLpzNXmmGhdLCjy99XK8+gjHdZHkOeD/FIjRPRZ7Jl0tdwdqFEwzRVCzL2uelMVMd3UaZ+d4I4Kf+J464piO//jxx48Fs/mG3zr5ba9m2S+6gvUZJq4j+0uJ+jf6cG/x2G9XSybqYQRwvxfNquKB4TcKiGVH5+ZbJT4ASkARadwoSPMGfvMPje+X2zAziSzXfsxYfIQKf6iJ9p7VavlDGi+Acr4kwFXW5IfQs4uGk6AVQFsoZK3o1hhLOkuOwWQEWhDQGNLXwJbFqXfELOnUQvM0Z5NUm46bjAAi4g+X9gLPNR/KjzXuuTTaWYrQEjXLb7PxS0sIttAb1w+sTXXtc1kDIsABC6KcsyGlEwji5sLkbkUa="},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{ASIABBKK02W42Q3IPSPG}{AQAAABAAA fkhIiUwQY32Zu9e4a86g9r3WpTzfE1aXljVcgn8O}{AQAAABAAA aSqfp/GTZbJP+tXPNCZ9GoveoM0vgxtlYXdzPQ2uYNMPPgUkt0VT7SoTLasAo7iVqWWREOUC6DEenlcgDEKyzIEgQW5Ju/b9K/Z176uD2HJYCfq/lyowHtt5PvJi7LRuf/urSorGbTcqNUvPi42YP1Ps/4F6He9hQA1io3EAGBC3ICGHXWf2IlvFoTNUyPTqhjnPEKMWZ42jblqNAdD7hLpzNXmmGhdLCjy99XK8+gjHdZHkOeD/FIjRPRZ7Jl0tdwdqFEwzRVCzL2uelMVMd3UaZ+d4I4Kf+J464piO//jxx48Fs/mG3zr5ba9m2S+6gvUZJq4j+0uJ+jf6cG/x2G9XSybqYQRwvxfNquKB4TcKiGVH5+ZbJT4ASkARadwoSPMGfvMPje+X2zAziSzXfsxYfIQKf6iJ9p7VavlDGi+Acr4kwFXW5IfQs4uGk6AVQFsoZK3o1hhLOkuOwWQEWhDQGNLXwJbFqXfELOnUQvM0Z5NUm46bjAAi4g+X9gLPNR/KjzXuuTTaWYrQEjXLb7PxS0sIttAb1w+sTXXtc1kDIsABC6KcsyGlEwji5sLkbkUa=}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"ASIABBKK02W42Q3IPSPG:fkhIiUwQY32Zu9e4a86g9r3WpTzfE1aXljVcgn8O:aSqfp/GTZbJP+tXPNCZ9GoveoM0vgxtlYXdzPQ2uYNMPPgUkt0VT7SoTLasAo7iVqWWREOUC6DEenlcgDEKyzIEgQW5Ju/b9K/Z176uD2HJYCfq/lyowHtt5PvJi7LRuf/urSorGbTcqNUvPi42YP1Ps/4F6He9hQA1io3EAGBC3ICGHXWf2IlvFoTNUyPTqhjnPEKMWZ42jblqNAdD7hLpzNXmmGhdLCjy99XK8+gjHdZHkOeD/FIjRPRZ7Jl0tdwdqFEwzRVCzL2uelMVMd3UaZ+d4I4Kf+J464piO//jxx48Fs/mG3zr5ba9m2S+6gvUZJq4j+0uJ+jf6cG/x2G9XSybqYQRwvxfNquKB4TcKiGVH5+ZbJT4ASkARadwoSPMGfvMPje+X2zAziSzXfsxYfIQKf6iJ9p7VavlDGi+Acr4kwFXW5IfQs4uGk6AVQFsoZK3o1hhLOkuOwWQEWhDQGNLXwJbFqXfELOnUQvM0Z5NUm46bjAAi4g+X9gLPNR/KjzXuuTTaWYrQEjXLb7PxS0sIttAb1w+sTXXtc1kDIsABC6KcsyGlEwji5sLkbkUa="},
},
{
name: "invalid pattern",
input: `
aws credentials{
id: ASIABBKK02W42Q3IPSPG
secret: $YenOG.PKHl7LcdVYsjaR4LgQiZ1zw3MAnMyiondXC63;
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
func TestAWSSessionKey_WithAllowedAccounts(t *testing.T) {
accounts := []string{"123456789012", "999888777666"}
s := New(WithAllowedAccounts(accounts))
// Test that allowed accounts are properly configured
shouldSkip := s.ShouldSkipAccount("123456789012")
require.False(t, shouldSkip)
require.True(t, s.IsInAllowList("123456789012"))
// Test that non-allowed accounts are skipped
shouldSkip = s.ShouldSkipAccount("111222333444")
require.True(t, shouldSkip)
require.False(t, s.IsInAllowList("111222333444"))
}
func TestAWSSessionKey_WithDeniedAccounts(t *testing.T) {
accounts := []string{"123456789012", "999888777666"}
s := New(WithDeniedAccounts(accounts))
// Test that denied accounts are properly skipped
shouldSkip := s.ShouldSkipAccount("123456789012")
require.True(t, shouldSkip)
require.True(t, s.IsInDenyList("123456789012"))
// Test that non-denied accounts are not skipped
shouldSkip = s.ShouldSkipAccount("111222333444")
require.False(t, shouldSkip)
require.False(t, s.IsInDenyList("111222333444"))
}
================================================
FILE: pkg/detectors/aws/utils.go
================================================
package aws
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base32"
"encoding/binary"
"encoding/hex"
"fmt"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
)
// ResourceTypes derived from: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-unique-ids
var ResourceTypes = map[string]string{
"ABIA": "AWS STS service bearer token",
"ACCA": "Context-specific credential",
"AGPA": "User group",
"AIDA": "IAM user",
"AIPA": "Amazon EC2 instance profile",
"AKIA": "Access key",
"ANPA": "Managed policy",
"ANVA": "Version in a managed policy",
"APKA": "Public key",
"AROA": "Role",
"ASCA": "Certificate",
"ASIA": "Temporary (AWS STS) access key IDs",
}
// UrlEncodedReplacer helps capture base64-encoded results that may be url-encoded.
// TODO: Add this as a decoder, or make it a more generic.
var UrlEncodedReplacer = strings.NewReplacer(
"%2B", "+",
"%2b", "+",
"%2F", "/",
"%2f", "/",
"%3d", "=",
"%3D", "=",
)
// Hashes, like those for git, do technically match the secret pattern.
// But they are extremely unlikely to be generated as an actual AWS secret.
// So when we find them, if they're not verified, we should ignore the result.
var FalsePositiveSecretPat = regexp.MustCompile(`[a-f0-9]{40}`)
func GetAccountNumFromID(id string) (string, error) {
// Function to get the account number from an AWS ID (no verification required)
// Source: https://medium.com/@TalBeerySec/a-short-note-on-aws-key-id-f88cc4317489
if len(id) < 4 {
return "", fmt.Errorf("AWSID is too short")
}
if id[4] == 'I' || id[4] == 'J' {
return "", fmt.Errorf("can't get account number from AKIAJ/ASIAJ or AKIAI/ASIAI keys")
}
trimmedAWSID := id[4:]
decodedBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(strings.ToUpper(trimmedAWSID))
if err != nil {
return "", err
}
if len(decodedBytes) < 6 {
return "", fmt.Errorf("decoded AWSID is too short")
}
data := make([]byte, 8)
copy(data[2:], decodedBytes[0:6])
z := binary.BigEndian.Uint64(data)
const mask uint64 = 0x7fffffffff80
accountNum := (z & mask) >> 7
return fmt.Sprintf("%012d", accountNum), nil
}
func GetHash(input string) string {
data := []byte(input)
hasher := sha256.New()
hasher.Write(data)
return hex.EncodeToString(hasher.Sum(nil))
}
func GetHMAC(key []byte, data []byte) []byte {
hasher := hmac.New(sha256.New, key)
hasher.Write(data)
return hasher.Sum(nil)
}
func CleanResults(results []detectors.Result) []detectors.Result {
if len(results) == 0 {
return results
}
// For every ID, we want at most one result, preferably verified.
idResults := map[string]detectors.Result{}
for _, result := range results {
// Always accept the verified result as the result for the given ID.
if result.Verified {
idResults[result.Redacted] = result
continue
}
// Only include an unverified result if we don't already have a result for a given ID.
if _, exist := idResults[result.Redacted]; !exist {
idResults[result.Redacted] = result
}
}
var out []detectors.Result
for _, r := range idResults {
out = append(out, r)
}
return out
}
================================================
FILE: pkg/detectors/axonaut/axonaut.go
================================================
package axonaut
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"axonaut"}) + `\b([a-z0-9]{32})\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{"axonaut"}
}
// FromData will find and optionally verify Axonaut 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Axonaut,
Raw: []byte(resMatch),
}
if verify {
isVerified, err := verifyMatch(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(err, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://axonaut.com/api/v2/companies?type=all&sort=id", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("userApiKey", key)
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:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Axonaut
}
func (s Scanner) Description() string {
return "Axonaut is a service that provides business management solutions including CRM, invoicing, and accounting. Axonaut API keys can be used to access and manage business data through their API."
}
================================================
FILE: pkg/detectors/axonaut/axonaut_integration_test.go
================================================
//go:build detectors
// +build detectors
package axonaut
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAxonaut_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AXONAUT")
inactiveSecret := testSecrets.MustGetField("AXONAUT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a axonaut secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Axonaut,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a axonaut secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Axonaut,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Axonaut.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Axonaut.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/axonaut/axonaut_test.go
================================================
package axonaut
import (
"context"
"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"
)
func TestAxonaut_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the axonaut API
[DEBUG] Using Key=4ve4aj6v38uiadaq9hcgpupp2b3lh2k8
[INFO] Response received: 200 OK
`,
want: []string{"4ve4aj6v38uiadaq9hcgpupp2b3lh2k8"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{axonaut}{AQAAABAAA m7mnuk7p3buc87b2ok29e7ykp2xqkkx0}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"m7mnuk7p3buc87b2ok29e7ykp2xqkkx0"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the axonaut API
[DEBUG] Using Key=ASIABBKK02W42Q3IPSPG
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/aylien/aylien.go
================================================
package aylien
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"aylien"}) + `\b([a-z0-9]{32})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"aylien"}) + `\b([a-z0-9]{8})\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{"aylien"}
}
// FromData will find and optionally verify Aylien 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
resIdMatch := strings.TrimSpace(idMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Aylien,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch),
}
if verify {
isVerified, err := verifyMatch(ctx, client, resIdMatch, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(err, resMatch)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, id, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.aylien.com/news/stories", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("X-AYLIEN-NewsAPI-Application-ID", id)
req.Header.Add("X-AYLIEN-NewsAPI-Application-Key", key)
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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Aylien
}
func (s Scanner) Description() string {
return "Aylien is a text analysis platform that provides natural language processing and machine learning APIs. Aylien API keys can be used to access and analyze text data."
}
================================================
FILE: pkg/detectors/aylien/aylien_integration_test.go
================================================
//go:build detectors
// +build detectors
package aylien
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAylien_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AYLIEN")
id := testSecrets.MustGetField("AYLIEN_ID")
inactiveSecret := testSecrets.MustGetField("AYLIEN_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aylien secret %s within aylien %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Aylien,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a aylien secret %s within aylien %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Aylien,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Aylien.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Aylien.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/aylien/aylien_test.go
================================================
package aylien
import (
"context"
"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"
)
func TestAylien_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
# do not share these credentials
aylien credentials:
aylien key: cr479du2l9pkmhar8gw5hufofvwp86q9
aylien id: y3ejw028
# valid till Dec 2025
`,
want: []string{"cr479du2l9pkmhar8gw5hufofvwp86q9y3ejw028"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{aylien wmxv7ckn}{aylien AQAAABAAA i09t8rb5r7otvq8sdrfjunakcso157mh}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"i09t8rb5r7otvq8sdrfjunakcso157mhwmxv7ckn"},
},
{
name: "invalid pattern",
input: `
# do not share these credentials
aylien credentials:
aylien key: cr4U9du2l9pkmhar8gw5hufofvWp86q9
aylien id: y3ejwA8
# valid till Dec 2025
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/ayrshare/ayrshare.go
================================================
package ayrshare
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ayrshare"}) + `\b([A-Z0-9]{8}-[A-Z0-9]{8}-[A-Z0-9]{8}-[A-Z0-9]{8})\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{"ayrshare"}
}
// FromData will find and optionally verify Ayrshare 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Ayrshare,
Raw: []byte(resMatch),
}
if verify {
isVerified, extraData, err := verifyMatch(ctx, client, resMatch)
s1.Verified = isVerified
s1.ExtraData = extraData
s1.SetVerificationError(err, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, key string) (bool, map[string]string, error) {
// Reference: https://www.ayrshare.com/docs/apis/user/profile-details
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://app.ayrshare.com/api/user", http.NoBody)
if err != nil {
return false, nil, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
res, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, nil, err
}
var responseBody map[string]any
if err := json.Unmarshal(bodyBytes, &responseBody); err == nil {
if email, ok := responseBody["email"].(string); ok {
return true, map[string]string{"email": email}, nil
}
}
return true, nil, nil
case http.StatusUnauthorized:
return false, nil, nil
case http.StatusForbidden:
// Invalid Bearer tokens get a 403 Forbidden response despite what is stated in the docs.
// Documentation: https://www.ayrshare.com/docs/errors/errors-http
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, nil, err
}
if strings.Contains(string(bodyBytes), "API Key not valid") {
return false, nil, nil
}
}
return false, nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Ayrshare
}
func (s Scanner) Description() string {
return "Ayrshare provides social media management services. Ayrshare API keys can be used to manage social media accounts and posts."
}
================================================
FILE: pkg/detectors/ayrshare/ayrshare_integration_test.go
================================================
//go:build detectors
// +build detectors
package ayrshare
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAyrshare_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AYRSHARE_TOKEN")
inactiveSecret := testSecrets.MustGetField("AYRSHARE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a ayrshare secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Ayrshare,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a ayrshare secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Ayrshare,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Ayrshare.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Ayrshare.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/ayrshare/ayrshare_test.go
================================================
package ayrshare
import (
"context"
"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"
)
func TestAyrShare_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the ayrshare API
[DEBUG] Using Key=2FTJTA1C-BXO0DV4J-HGTP9E62-QHQSILY1
[INFO] Response received: 200 OK
`,
want: []string{"2FTJTA1C-BXO0DV4J-HGTP9E62-QHQSILY1"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{ayrshare}{AQAAABAAA I1WPQLUQ-NCNHEI13-1MF4HJZQ-EEDDVZYO}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"I1WPQLUQ-NCNHEI13-1MF4HJZQ-EEDDVZYO"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the ayrshare API
[DEBUG] Using Key=KRXaU9GK3f[yHG1FS$]bwhsIXdW22epH
[ERROR] Response received: 401 UnAuthorized
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/azure_batch/azurebatch.go
================================================
package azure_batch
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)
var (
defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
urlPat = regexp.MustCompile(`https://(.{1,50})\.(.{1,50})\.batch\.azure\.com`)
secretPat = regexp.MustCompile(`[A-Za-z0-9+/=]{88}`)
)
// 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{".batch.azure.com"}
}
// FromData will find and optionally verify Azurebatch 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)
urlMatches := urlPat.FindAllStringSubmatch(dataStr, -1)
secretMatches := secretPat.FindAllStringSubmatch(dataStr, -1)
for _, urlMatch := range urlMatches {
for _, secretMatch := range secretMatches {
endpoint := urlMatch[0]
accountName := urlMatch[1]
accountKey := secretMatch[0]
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AzureBatch,
Raw: []byte(endpoint),
RawV2: []byte(endpoint + accountKey),
Redacted: endpoint,
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, err := verifyMatch(ctx, client, endpoint, accountName, accountKey)
s1.Verified = isVerified
s1.SetVerificationError(err)
}
results = append(results, s1)
if s1.Verified {
break
}
}
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, endpoint, accountName, accountKey string) (bool, error) {
// Reference: https://learn.microsoft.com/en-us/rest/api/batchservice/application/list
url := fmt.Sprintf("%s/applications?api-version=2020-09-01.12.0", endpoint)
date := time.Now().UTC().Format(http.TimeFormat)
stringToSign := fmt.Sprintf(
"GET\n\n\n\n\napplication/json\n%s\n\n\n\n\n\n%s\napi-version:%s",
date,
strings.ToLower(fmt.Sprintf("/%s/applications", accountName)),
"2020-09-01.12.0",
)
key, _ := base64.StdEncoding.DecodeString(accountKey)
h := hmac.New(sha256.New, key)
h.Write([]byte(stringToSign))
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return false, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("SharedKey %s:%s", accountName, signature))
req.Header.Set("Date", date)
resp, err := client.Do(req)
if err != nil {
// If the host is not found, we can assume that the endpoint is invalid
if strings.Contains(err.Error(), "no such host") {
return false, nil
}
return false, err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusForbidden:
// Key is either invalid or the account is disabled.
return false, nil
default:
return false, fmt.Errorf("unexpected status code %d for %s", resp.StatusCode, url)
}
}
func (s Scanner) IsFalsePositive(_ detectors.Result) (bool, string) {
return false, ""
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureBatch
}
func (s Scanner) Description() string {
return "Azure Batch is a cloud service that provides large-scale parallel and high-performance computing (HPC) applications efficiently in the cloud. Azure Batch account keys can be used to manage and control access to these resources."
}
================================================
FILE: pkg/detectors/azure_batch/azurebatch_integration_test.go
================================================
//go:build detectors
// +build detectors
package azure_batch
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAzurebatch_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)
}
url := testSecrets.MustGetField("AZUREBATCH_URL")
secret := testSecrets.MustGetField("AZUREBATCH_KEY")
inactiveSecret := testSecrets.MustGetField("AZUREBATCH_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azurebatch secret %s and %s within", url, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureBatch,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azurebatch secret %s and %s within but not valid", url, inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureBatch,
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,
},
}
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("AzureBatch.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "RawV2", "Raw", "Redacted", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AzureBatch.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azure_batch/azurebatch_test.go
================================================
package azure_batch
import (
"context"
"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"
)
func TestAzureBatch_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the ayrshare API
[DEBUG] Using Secret = BXIMbhBlC3=5hIbqCEKvq7opaV2ZfO0XWbcnasZmPm/AJfQqdcnt/AVmKkJ8Qw80Zc1rQDaw+2Ytxc1hDq1m/LB0
[INFO] https://JrxlYxT+0hW.YSA.batch.azure.com
[INFO] Response received: 200 OK
`,
want: []string{"https://JrxlYxT+0hW.YSA.batch.azure.comBXIMbhBlC3=5hIbqCEKvq7opaV2ZfO0XWbcnasZmPm/AJfQqdcnt/AVmKkJ8Qw80Zc1rQDaw+2Ytxc1hDq1m/LB0"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{https://pb0bik2a59qznkh87pdd6twjlgzpmxz.pfv9bpr2hujs.batch.azure.com}{AQAAABAAA XJc2nGZvqPAXYfHxsiwUDBA4ynHzGc9nQl1Ih16lk19=2+qqeJUDp5eBxWVrE0LQYlnbeu/orbEtblFL218S4Wko}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"https://pb0bik2a59qznkh87pdd6twjlgzpmxz.pfv9bpr2hujs.batch.azure.comXJc2nGZvqPAXYfHxsiwUDBA4ynHzGc9nQl1Ih16lk19=2+qqeJUDp5eBxWVrE0LQYlnbeu/orbEtblFL218S4Wko"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the ayrshare API
[DEBUG] Using Secret=BXIMbhBlC3=5hIbqCEKvq7op!V2ZfO0XWbcnasZmPm/AJfQqdcnt/AVmKkJ8Qw80Zc1rQDaw+2Ytxc1hDq1m/
[INFO] http://invalid.this.batch.azure.com
[INFO] Response received: 200 OK
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/azure_cosmosdb/azure_cosmosdb.go
================================================
package azure_cosmosdb
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
}
var (
defaultClient = common.SaneHttpClient()
dbKeyPattern = regexp.MustCompile(`([A-Za-z0-9]{86}==)`)
// account name can contain only lowercase letters, numbers and the `-` character, must be between 3 and 44 characters long.
accountUrlPattern = regexp.MustCompile(`([a-z0-9-]{3,44}\.(?:documents|table\.cosmos)\.azure\.com)`)
invalidHosts = simple.NewCache[struct{}]()
errNoHost = errors.New("no such host")
)
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureCosmosDBKeyIdentifiable
}
func (s Scanner) Description() string {
return "Azure Cosmos DB is a globally distributed, multi-model database service offered by Microsoft. CosmosDB keys and connection string are used to connect with Cosmos DB."
}
func (s Scanner) Keywords() []string {
return []string{".documents.azure.com", ".table.cosmos.azure.com"}
}
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
dataStr := string(data)
var uniqueKeyMatches, uniqueAccountMatches = make(map[string]struct{}), make(map[string]struct{})
for _, match := range dbKeyPattern.FindAllStringSubmatch(dataStr, -1) {
uniqueKeyMatches[match[1]] = struct{}{}
}
for _, match := range accountUrlPattern.FindAllStringSubmatch(dataStr, -1) {
uniqueAccountMatches[match[1]] = struct{}{}
}
for key := range uniqueKeyMatches {
for accountUrl := range uniqueAccountMatches {
if invalidHosts.Exists(accountUrl) {
delete(uniqueAccountMatches, accountUrl)
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AzureCosmosDBKeyIdentifiable,
Raw: []byte(key),
RawV2: []byte("key: " + key + " account_url: " + accountUrl), // key: account_url:
ExtraData: map[string]string{},
}
if verify {
var verified bool
var verificationErr error
client := s.getClient()
// perform verification based on db type
if strings.Contains(accountUrl, ".documents.azure.com") {
verified, verificationErr = verifyCosmosDocumentDB(client, accountUrl, key)
s1.ExtraData["DB Type"] = "Document"
} else if strings.Contains(accountUrl, ".table.cosmos.azure.com") {
verified, verificationErr = verifyCosmosTableDB(client, accountUrl, key)
s1.ExtraData["DB Type"] = "Table"
}
s1.Verified = verified
if verificationErr != nil {
if errors.Is(verificationErr, errNoHost) {
invalidHosts.Set(accountUrl, struct{}{})
continue
}
s1.SetVerificationError(verificationErr)
}
}
results = append(results, s1)
}
}
return results, nil
}
// documentation: https://learn.microsoft.com/en-us/rest/api/cosmos-db/list-databases
func verifyCosmosDocumentDB(client *http.Client, accountUrl, key string) (bool, error) {
// decode the base64 encoded key
decodedKey, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return false, fmt.Errorf("failed to decode key: %v", err)
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s:443/dbs", accountUrl), nil)
if err != nil {
return false, fmt.Errorf("failed to create request: %v", err)
}
dateRFC1123 := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
authHeader := fmt.Sprintf("type=master&ver=1.0&sig=%s", url.QueryEscape(createDocumentsSignature(decodedKey, dateRFC1123)))
// required headers
// docs: https://learn.microsoft.com/en-us/rest/api/cosmos-db/common-cosmosdb-rest-request-headers
req.Header.Set("Authorization", authHeader)
req.Header.Set("x-ms-date", dateRFC1123)
req.Header.Set("x-ms-version", "2018-12-31")
resp, err := client.Do(req)
if err != nil {
// lookup foo.documents.azure.com: no such host
if strings.Contains(err.Error(), "no such host") {
return false, errNoHost
}
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
// Check response status code
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
func createDocumentsSignature(decodedKey []byte, dateRFC1123 string) string {
stringToSign := fmt.Sprintf(
"%s\n%s\n%s\n%s\n\n",
strings.ToLower(http.MethodGet),
strings.ToLower("dbs"),
"",
strings.ToLower(dateRFC1123),
)
// compute HMAC-SHA256 signature
mac := hmac.New(sha256.New, decodedKey)
mac.Write([]byte(stringToSign))
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
================================================
FILE: pkg/detectors/azure_cosmosdb/azure_cosmosdb_integration_test.go
================================================
//go:build detectors
// +build detectors
package azure_cosmosdb
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/detectorspb"
)
func TestCosmosDB_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)
}
key := testSecrets.MustGetField("COSMOSDB_KEY")
accountUrl := testSecrets.MustGetField("COSMOSDB_ACCOUNT")
inactiveKey := testSecrets.MustGetField("COSMOSDB_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",
s: Scanner{},
args: args{
ctx: ctx,
data: []byte(fmt.Sprintf("You can find a cosmosdb key: %s and account url: %s within", key, accountUrl)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureCosmosDBKeyIdentifiable,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: ctx,
data: []byte(fmt.Sprintf("You can find a cosmosdb key: %s and accounturl: %s within but not valid", inactiveKey, accountUrl)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureCosmosDBKeyIdentifiable,
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,
},
}
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("CosmosDB.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("CosmosDB.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azure_cosmosdb/azure_cosmosdb_test.go
================================================
package azure_cosmosdb
import (
"context"
"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"
)
func TestCosmosDB_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid document db pattern",
input: `
Cluster name: Cluster name must be at least 3 characters and at most 40 characters.
Cluster name must only contain lowercase letters, numbers, and hyphens.
The cluster name must not start or end in a hyphen.
// config
cosmosKey: FakeeP35zYGPXaEUfakeU7S8kcOY7NI7id8ddbHfakeAifake8Bbql1mXhMF2t0wQ0FAKEPQrwZZACDb3msoAg==
https://trufflesecurity-fake.documents.azure.com:443`,
want: []string{"key: FakeeP35zYGPXaEUfakeU7S8kcOY7NI7id8ddbHfakeAifake8Bbql1mXhMF2t0wQ0FAKEPQrwZZACDb3msoAg== account_url: trufflesecurity-fake.documents.azure.com"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{jc0338vpo7bd3rn99vu2trdbo.table.cosmos.azure.com}{AQAAABAAA tiHd2l1I3MptBj4s1zomhyIAuCJmR1bzxvGluBVW2k0JJ7Z6vmybKYiM7OY5HtDkvLVxyDD2ACW0GW2fug0cET==}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"key: tiHd2l1I3MptBj4s1zomhyIAuCJmR1bzxvGluBVW2k0JJ7Z6vmybKYiM7OY5HtDkvLVxyDD2ACW0GW2fug0cET== account_url: jc0338vpo7bd3rn99vu2trdbo.table.cosmos.azure.com"},
},
{
name: "valid table db pattern",
input: `
Cluster name: Cluster name must be at least 3 characters and at most 40 characters.
Cluster name must only contain lowercase letters, numbers, and hyphens.
The cluster name must not start or end in a hyphen.
// config
cosmosKey: FakeeP35zYGPXaEUfakeU7S8kcOY7NI7id8ddbHfakeAifake8Bbql1mXhMF2t0wQ0FAKEPQrwZZACDb3msoAg==
https://trufflesecurity-fake.table.cosmos.azure.com:443`,
want: []string{"key: FakeeP35zYGPXaEUfakeU7S8kcOY7NI7id8ddbHfakeAifake8Bbql1mXhMF2t0wQ0FAKEPQrwZZACDb3msoAg== account_url: trufflesecurity-fake.table.cosmos.azure.com"},
},
{
name: "invalid pattern",
input: `
FakeeP35zYGPXaEUfakeU7S8kcOY7I7id8ddbHfakeAifake8Bbql1mXhMF2t0wQ0FAKEPQrwZZACDb3msoAg==
https://not-a-host.documents.azure.com:443`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/azure_cosmosdb/table.go
================================================
package azure_cosmosdb
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"net/http"
"strings"
"time"
)
func verifyCosmosTableDB(client *http.Client, accountUrl, key string) (bool, error) {
// decode the base64 encoded key
decodedKey, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return false, fmt.Errorf("failed to decode key: %v", err)
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s:443/Tables", accountUrl), nil)
if err != nil {
return false, fmt.Errorf("failed to create request: %v", err)
}
// extract abc123 from abc123.table.cosmos.azure.com
accountName := strings.TrimPrefix(accountUrl, ".table.cosmos.azure.com")
dateRFC1123 := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
authHeader := fmt.Sprintf("SharedKeyLite %s:%s", accountName, createTablesSignature(decodedKey, accountName, dateRFC1123))
// required headers
// docs: https://learn.microsoft.com/en-us/rest/api/cosmos-db/common-cosmosdb-rest-request-headers
req.Header.Set("Authorization", authHeader)
req.Header.Set("x-ms-date", dateRFC1123)
req.Header.Set("x-ms-version", "2019-02-02")
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
// lookup foo.table.cosmos.azure.com: no such host
if strings.Contains(err.Error(), "no such host") {
return false, errNoHost
}
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
// Check response status code
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
func createTablesSignature(decodedKey []byte, accountName, dateRFC1123 string) string {
// create string to sign (method + date)
stringToSign := fmt.Sprintf("%s\n%s", dateRFC1123, fmt.Sprintf("/%s/Tables", accountName))
// Compute HMAC-SHA256 signature
h := hmac.New(sha256.New, decodedKey)
h.Write([]byte(stringToSign))
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
return signature
}
================================================
FILE: pkg/detectors/azure_entra/common.go
================================================
package azure_entra
import (
"fmt"
"io"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"golang.org/x/sync/singleflight"
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
)
const uuidStr = `[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}`
var (
// Tenants can be identified with a UUID or an `*.onmicrosoft.com` domain.
//
// See:
// https://learn.microsoft.com/en-us/partner-center/account-settings/find-ids-and-domain-names#find-the-microsoft-azure-ad-tenant-id-and-primary-domain-name
// https://learn.microsoft.com/en-us/microsoft-365/admin/setup/domains-faq?view=o365-worldwide#why-do-i-have-an--onmicrosoft-com--domain
tenantIdPat = regexp.MustCompile(fmt.Sprintf(
//language=regexp
`(?i)(?:(?:login\.microsoftonline\.com/|(?:login|sts)\.windows\.net/|(?:t[ae]n[ae]nt(?:[ ._-]?id)?|\btid)(?:.|\s){0,60}?)(%s)|https?://(%s)|X-AnchorMailbox(?:.|\s){0,60}?@(%s)|/(%s)/(?:oauth2/v2\.0|B2C_1\w+|common|discovery|federationmetadata|kerberos|login|openid/|reprocess|resume|saml2|token|uxlogout|v2\.0|wsfed))`,
uuidStr,
uuidStr,
uuidStr,
uuidStr,
))
tenantOnMicrosoftPat = regexp.MustCompile(`([\w-]+\.onmicrosoft\.com)`)
clientIdPat = regexp.MustCompile(fmt.Sprintf(
`(?i)(?:(?:app(?:lication)?|client)(?:[ ._-]?id)?|username| -u)(?:.|\s){0,45}?(%s)`, uuidStr))
)
// FindTenantIdMatches returns a list of potential tenant IDs in the provided |data|.
func FindTenantIdMatches(data string) map[string]struct{} {
uniqueMatches := make(map[string]struct{})
for _, match := range tenantIdPat.FindAllStringSubmatch(data, -1) {
var m string
if match[1] != "" {
m = strings.ToLower(match[1])
} else if match[2] != "" {
m = strings.ToLower(match[2])
} else if match[3] != "" {
m = strings.ToLower(match[3])
} else if match[4] != "" {
m = strings.ToLower(match[4])
}
if _, ok := detectors.UuidFalsePositives[detectors.FalsePositive(m)]; ok {
continue
} else if detectors.StringShannonEntropy(m) < 3 {
continue
}
uniqueMatches[m] = struct{}{}
}
for _, match := range tenantOnMicrosoftPat.FindAllStringSubmatch(data, -1) {
uniqueMatches[match[1]] = struct{}{}
}
return uniqueMatches
}
// FindClientIdMatches returns a list of potential client UUIDs in the provided |data|.
func FindClientIdMatches(data string) map[string]struct{} {
uniqueMatches := make(map[string]struct{})
for _, match := range clientIdPat.FindAllStringSubmatch(data, -1) {
m := strings.ToLower(match[1])
if _, ok := detectors.UuidFalsePositives[detectors.FalsePositive(m)]; ok {
continue
} else if detectors.StringShannonEntropy(m) < 3 {
continue
}
uniqueMatches[m] = struct{}{}
}
return uniqueMatches
}
var (
tenantCache = simple.NewCache[bool]()
tenantGroup singleflight.Group
)
// TenantExists returns whether the tenant exists according to Microsoft's well-known OpenID endpoint.
func TenantExists(ctx context.Context, client *http.Client, tenant string) bool {
// Use cached value where possible.
if tenantExists, isCached := tenantCache.Get(tenant); isCached {
return tenantExists
}
// https://www.codingexplorations.com/blog/understanding-singleflight-in-golang-a-solution-for-eliminating-redundant-work
tenantExists, _, _ := tenantGroup.Do(tenant, func() (interface{}, error) {
result := queryTenant(ctx, client, tenant)
tenantCache.Set(tenant, result)
return result, nil
})
return tenantExists.(bool)
}
func queryTenant(ctx context.Context, client *http.Client, tenant string) bool {
logger := ctx.Logger().WithName("azure").WithValues("tenant", tenant)
tenantUrl := fmt.Sprintf("https://login.microsoftonline.com/%s/.well-known/openid-configuration", tenant)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, tenantUrl, nil)
if err != nil {
return false
}
res, err := client.Do(req)
if err != nil {
logger.Error(err, "Failed to check if tenant exists")
return false
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
return true
case http.StatusBadRequest:
logger.V(4).Info("Tenant does not exist.")
return false
default:
bodyBytes, _ := io.ReadAll(res.Body)
logger.Error(nil, "WARNING: Unexpected response when checking if tenant exists", "status_code", res.StatusCode, "body", string(bodyBytes))
return false
}
}
================================================
FILE: pkg/detectors/azure_entra/common_test.go
================================================
package azure_entra
import (
"testing"
"github.com/google/go-cmp/cmp"
)
type testCase struct {
Input string
Expected map[string]struct{}
}
func runPatTest(t *testing.T, tests map[string]testCase, matchFunc func(data string) map[string]struct{}) {
t.Helper()
for name, test := range tests {
t.Run(name, func(t *testing.T) {
matches := matchFunc(test.Input)
if len(matches) == 0 {
if len(test.Expected) != 0 {
t.Fatalf("no matches found, expected: %v", test.Expected)
return
} else {
return
}
}
if diff := cmp.Diff(test.Expected, matches); diff != "" {
t.Errorf("expected: %s, actual: %s", test.Expected, matches)
return
}
})
}
}
func Test_FindTenantIdMatches(t *testing.T) {
cases := map[string]testCase{
// Tenant ID
"audience": {
Input: `az offazure hyperv site create --location "eastus" --service-principal-identity-details \
application-id="cbcfc473-97da-45dd-8a00-3612d1ddf35a" \
audience="https://bced5192-08c4-4470-9a94-666fea59efb07/aadapp" `,
Expected: map[string]struct{}{
"bced5192-08c4-4470-9a94-666fea59efb0": {},
},
},
"tenant": {
Input: ` "cas.authn.azure-active-directory.login-url=https://login.microsoftonline.com/common/",
"cas.authn.azure-active-directory.tenant=8e439f30-da7a-482c-bd23-e45d0a732000"`,
Expected: map[string]struct{}{
"8e439f30-da7a-482c-bd23-e45d0a732000": {},
},
},
"tanentId": {
Input: `azure.grantType=client_credentials
azure.tanentId=029e3b51-60dd-47aa-81ad-3c15b389db86`,
Expected: map[string]struct{}{
"029e3b51-60dd-47aa-81ad-3c15b389db86": {},
},
},
"tenantid": {
Input: ` file:
folder-location: test
tenantid: ${vcap.services.user-authentication-service.credentials.tenantid:317fb200-a693-4062-a4fb-9d131fcd2d3c}`,
Expected: map[string]struct{}{
"317fb200-a693-4062-a4fb-9d131fcd2d3c": {},
},
},
"tenant id": {
Input: `1. Enter the tenant id "2ce99e96-b41b-47a0-b37c-16a22bceb8c0"`,
Expected: map[string]struct{}{
"2ce99e96-b41b-47a0-b37c-16a22bceb8c0": {},
},
},
"tenant_id": {
Input: `location = "eastus"
subscription_id = "47ab1364-000d-4a53-838d-1537b1e3b49f"
tenant_id = "57aabdfc-6ce0-4828-94a2-9abe277892ec"`,
Expected: map[string]struct{}{
"57aabdfc-6ce0-4828-94a2-9abe277892ec": {},
},
},
"tenant-id": {
Input: ` active-directory:
enabled: true
profile:
tenant-id: c32654ed-6931-4bae-bb23-a8b9e420e0f4
credential:`,
Expected: map[string]struct{}{
"c32654ed-6931-4bae-bb23-a8b9e420e0f4": {},
},
},
"tid": {
Input: ` "sub": "jIzit1WEdXqAH9KZXz-e-UcqsVa1pyPoh-2hw3xjEO4",
"tenant_region_scope": "AS",
"tid": "974fde14-c3a4-481b-9b03-cfce18213a07",
"uti": "2Y26RWHsWEiqhD2vi_PFAg",`,
Expected: map[string]struct{}{
"974fde14-c3a4-481b-9b03-cfce18213a07": {},
},
},
"login.microsoftonline.com": {
Input: ` auth: {
authority: 'https://login.microsoftonline.com/7bb339cb-e94c-4a85-884c-48ebd9bb28c3',
redirectUri: 'http://localhost:8080/landing'
`,
Expected: map[string]struct{}{
"7bb339cb-e94c-4a85-884c-48ebd9bb28c3": {},
},
},
"login.windows.net": {
Input: `az offazure hyperv site create --location "eastus" --service-principal-identity-details aad-authority="https://login.windows.net/7bb339cb-e94c-4a85-884c-48ebd9bb28c3" application-id="e9f013df-2a2a-4871-b766-e79867f30348" \'`,
Expected: map[string]struct{}{
"7bb339cb-e94c-4a85-884c-48ebd9bb28c3": {},
},
},
"sts.windows.net": {
Input: `{
"aud": "00000003-0000-0000-c000-000000000000",
"iss": "https://sts.windows.net/974fde14-c3a4-481b-9b03-cfce182c3a07/",
"iat": 1641799220,`,
Expected: map[string]struct{}{
"974fde14-c3a4-481b-9b03-cfce182c3a07": {},
},
},
"oauth paths": {
Input: ` "authPath": "/9b4bfaea-dd1c-4add-b1de-e10f51c65fd3/oauth2/v2.0/authorize",
/32896ed7-d559-401b-85cf-167143d61be0/B2C_1A_Tapio_Signin/v2.0
/461858f4-9c0d-46e0-a9e6-aefc4889aad6/B2C_1_sign_up_or_sign_in/SelfAsserted?tx=S
-ArgumentList "/3f548be2-31e9-4681-839e-bc80d461f367/common/oauth2/authorize"
"jwks_uri": "/6babcaad-604b-40ac-a9d7-9fd97c0b779f/discovery/keys",
MetadataLocation = "/b55f0c51-61a7-45c3-84df-33569b247796/federationmetadata/2007-06/federationmetadata.xml?appid=3245199b-1a5d-42df-93ce-e64ac7f5b938
"kerberos_endpoint": "/a4067d12-2fc0-4367-a213-9e4031cbc173/kerberos",
/b2326b8a-059d-48ca-96ac-8d8d5d841860/login
"userinfo_endpoint": "/6ba4caad-604b-40ac-a9d7-9fd97c0b779f/openid/userinfo"
…en-US","urlLogin":"/9673e9a8-aa57-4461-9336-5fd3f0034e18/reprocess?ctx=rQIIAZ2QvWvbQA…
/6c912b97-d9f0-4472-a96a-d82de2f1d438/resume?ctx=rQIIAZVTP
// /aa8306d8-5417-43cc-b8e8-7e77b918682c/v2.0/.well-known/openid-configuration
// /051aeb51-408b-403b-b95c-4ff3b303a08a/token
"/4a5378f9-29f4-4d3e-be89-669d03ada9d8/uxlogout"
/dc38a67a-f981-4e24-ba16-4443ada44484/wsfed
`,
Expected: map[string]struct{}{
"051aeb51-408b-403b-b95c-4ff3b303a08a": {},
"32896ed7-d559-401b-85cf-167143d61be0": {},
"3f548be2-31e9-4681-839e-bc80d461f367": {},
"461858f4-9c0d-46e0-a9e6-aefc4889aad6": {},
"4a5378f9-29f4-4d3e-be89-669d03ada9d8": {},
"6ba4caad-604b-40ac-a9d7-9fd97c0b779f": {},
"6babcaad-604b-40ac-a9d7-9fd97c0b779f": {},
"6c912b97-d9f0-4472-a96a-d82de2f1d438": {},
"9673e9a8-aa57-4461-9336-5fd3f0034e18": {},
"9b4bfaea-dd1c-4add-b1de-e10f51c65fd3": {},
"a4067d12-2fc0-4367-a213-9e4031cbc173": {},
"aa8306d8-5417-43cc-b8e8-7e77b918682c": {},
"b2326b8a-059d-48ca-96ac-8d8d5d841860": {},
"b55f0c51-61a7-45c3-84df-33569b247796": {},
"dc38a67a-f981-4e24-ba16-4443ada44484": {},
},
},
"x-anchor-mailbox": {
// The tenantID can be encoded in this parameter.
// https://github.com/AzureAD/microsoft-authentication-library-for-python/blob/95a63a7fe97d91b99979e5bf78e03f6acf40a286/msal/application.py#L185-L186
// https://github.com/silverhack/monkey365/blob/b3f43c4a2d014fcc3aae0a4103c8f2610fbb4980/core/utils/Get-MonkeySecCompBackendUri.ps1#L70
Input: ` User-Agent:
- python-requests/2.31.0
X-AnchorMailbox:
- Oid:2b9b0cb5-d707-42e3-9504-d9b76ac7bec5@86843c34-863b-44d3-bb14-4f14e7c0564d
x-client-current-telemetry:
- 4|84,3|`,
Expected: map[string]struct{}{
"86843c34-863b-44d3-bb14-4f14e7c0564d": {},
},
},
// Tenant onmicrosoft.com
"onmicrosoft tenant": {
Input: ` "oid": "7be15f3a-d9b5-4080-ba37-95aa2e3d244e",
"platf": "3",
"puid": "10032001170600C8",
"scp": "Files.Read Files.Read.All Files.Read.Selected Files.ReadWrite Files.ReadWrite.All Files.ReadWrite.AppFolder Files.ReadWrite.Selected profile User.Export.All User.Invite.All User.ManageIdentities.All User.Read User.Read.All User.ReadBasic.All openid email",
"signin_state": [
"kmsi"
],
"sub": "jIzit1WEdXqAH9KZXz-e-UcqsVa1pyPoh-2hw3xjEO4",
"tenant_region_scope": "AS",
"unique_name": "ben@xhoaxiuqng.onmicrosoft.com",
"uti": "2Y26RWHsWEiqhD2vi_PFAg",
"ver": "1.0",
"wids": [
"62e90394-69f5-4237-9190-012177145e10",
"b79fbf4d-3ef9-4689-8143-76b194e85509"
],`,
Expected: map[string]struct{}{
"xhoaxiuqng.onmicrosoft.com": {},
},
},
// Arbitrary test cases
"spacing": {
Input: `| Variable name | Description | Example value |
| ----------------- | ------------------------------------------------------------- | ------------------------------------- |
| AFASBaseUri | Base URI of the AFAS REST API endpoint for this environment | https://12345.rest.afas.online/ProfitRestServices |
| AFASToke | App token in XML format for this environment | \\1\\D5R324DD5F4TRD945E530ED3CDD70D94BBDEC4C732B43F285ECB12345678\\ |
| AADtenantID | Id of the Azure tenant | 12fc345b-0c67-4cde-8902-dabf2cad34b5 |
| AADAppId | Id of the Azure app | f12345c6-7890-1f23-b456-789eb0bb1c23 |
| AADAppSecret | Secret of the Azure app | G1X2HsBw-co3dTIB45RE6vY.mSU~6u.7.8 |`,
Expected: map[string]struct{}{
"12fc345b-0c67-4cde-8902-dabf2cad34b5": {},
},
},
"newline": {
Input: ` {\n \"mode\": \"Manual\"\n },\n \"bootstrapProfile\": {\n \"artifactSource\":
\"Direct\"\n }\n },\n \"identity\": {\n \"type\": \"SystemAssigned\",\n
\ \"principalId\":\"00000000-0000-0000-0000-000000000001\",\n \"tenantId\":
\"d0a69dfd-9b9e-4833-9c33-c7903dd2e012\"\n },\n \"sku\": {\n \"name\": \"Base\",\n
\ \"tier\": \"Free\"\n }\n}"
headers:`,
Expected: map[string]struct{}{
"d0a69dfd-9b9e-4833-9c33-c7903dd2e012": {},
},
},
// False positives
"tid shouldn't match clientId": {
Input: `"userId": "jdoe@businesscorp.ca", "isUserIdDisplayable": true, "isMRRT": true, "_clientId": "d3590ed6-52b3-4102-aeff-aad2292ab01c", }`,
Expected: nil,
},
"tid shouldn't match subscription_id": {
Input: `location = "eastus"
subscription_id = "47ab1364-000d-4a53-838d-1537b1e3b49f"`,
Expected: nil,
},
}
runPatTest(t, cases, FindTenantIdMatches)
}
func Test_FindClientIdMatches(t *testing.T) {
cases := map[string]testCase{
"app": {
Input: `var app = "4ba50db1-3f3f-4521-8a9a-1be0864d922a"`,
Expected: map[string]struct{}{
"4ba50db1-3f3f-4521-8a9a-1be0864d922a": {},
},
},
"appid": {
Input: `The output includes credentials that you must protect. Be sure that you do not include these credentials in your code or check the credentials into your source control. For more information, see https://aka.ms/azadsp-cli
{
"appId": "4ba50db1-3f3f-4521-8a9a-1be0864d922a",
"displayName": "azure-cli-2022-12-02-15-40-24",`,
Expected: map[string]struct{}{
"4ba50db1-3f3f-4521-8a9a-1be0864d922a": {},
},
},
"app_id": {
Input: `msal:
app_id: 'b9cbc91c-c890-4824-a487-91611bb0615a'`,
Expected: map[string]struct{}{
"b9cbc91c-c890-4824-a487-91611bb0615a": {},
},
},
"application": {
Input: `const application = \x60902aeb6d-29c7-4f6e-849d-4b933117e320\x60`,
Expected: map[string]struct{}{
"902aeb6d-29c7-4f6e-849d-4b933117e320": {},
},
},
"applicationid": {
Input: `# Login using Service Principal
$ApplicationId = "1e002bca-c6e2-446e-a29e-a221909fe8aa"`,
Expected: map[string]struct{}{
"1e002bca-c6e2-446e-a29e-a221909fe8aa": {},
},
},
"application id": {
Input: `The application id is "029e3b51-60dd-47aa-81ad-3c15b389db86", you need to`,
Expected: map[string]struct{}{
"029e3b51-60dd-47aa-81ad-3c15b389db86": {},
},
},
"application_id": {
Input: ` credential:
application_id: |
bafe0126-03eb-4917-b3ff-4601c4e8f12f`,
Expected: map[string]struct{}{
"bafe0126-03eb-4917-b3ff-4601c4e8f12f": {},
},
},
"application-id": {
Input: `vcap.services.msal.application-id: 0704100e-7e76-4e62-bfb6-70bfd33906e2`,
Expected: map[string]struct{}{
"0704100e-7e76-4e62-bfb6-70bfd33906e2": {},
},
},
"client": {
Input: `String client = "902aeb6d-29c7-4f6e-849d-4b933117e320";`,
Expected: map[string]struct{}{
"902aeb6d-29c7-4f6e-849d-4b933117e320": {},
},
},
"clientid": {
Input: `export const msalConfig = {
auth: {
clientId: '82c54108-535c-40b2-87dc-2db599df3810',`,
Expected: map[string]struct{}{
"82c54108-535c-40b2-87dc-2db599df3810": {},
},
},
"client id": {
Input: `The client ID is: a54e584d-6fc4-464c-8479-dc67b5d87ab9`,
Expected: map[string]struct{}{
"a54e584d-6fc4-464c-8479-dc67b5d87ab9": {},
},
},
"client_id": {
Input: `location = "eastus"
client_id = "89d5bd08-0d51-42cd-8eab-382c3ce11199"
subscription_id = "47ab1364-000d-4a53-838d-1537b1e3b49f"
`,
Expected: map[string]struct{}{
"89d5bd08-0d51-42cd-8eab-382c3ce11199": {},
},
},
"client-id": {
Input: `@TestPropertySource(properties = {
"cas.authn.azure-active-directory.client-id=532c556b-1260-483f-9695-68d087fcd965",
"cas.authn.azure-active-directory.client-secret`,
Expected: map[string]struct{}{
"532c556b-1260-483f-9695-68d087fcd965": {},
},
},
"username": {
Input: `az login --service-principal --username "21e144ac-532d-49ad-ba15-1c40694ce8b1" --password`,
Expected: map[string]struct{}{
"21e144ac-532d-49ad-ba15-1c40694ce8b1": {},
},
},
"-u": {
Input: `az login --service-principal -u "21e144ac-532d-49ad-ba15-1c40694ce8b1" -p`,
Expected: map[string]struct{}{
"21e144ac-532d-49ad-ba15-1c40694ce8b1": {},
},
},
// Arbitrary test cases
"spacing": {
Input: `| Variable name | Description | Example value |
| ----------------- | ------------------------------------------------------------- | ------------------------------------- |
| AFASBaseUri | Base URI of the AFAS REST API endpoint for this environment | https://12345.rest.afas.online/ProfitRestServices |
| AFASToke | App token in XML format for this environment | \\1\\D5R324DD5F4TRD945E530ED3CDD70D94BBDEC4C732B43F285ECB12345678\\ |
| AADtenantID | Id of the Azure tenant | 12fc345b-0c67-4cde-8902-dabf2cad34b5 |
| AADAppId | Id of the Azure app | f12345c6-7890-1f23-b456-789eb0bb1c23 |
| AADAppSecret | Secret of the Azure app | G1X2HsBw-co3dTIB45RE6vY.mSU~6u.7.8 |`,
Expected: map[string]struct{}{
"f12345c6-7890-1f23-b456-789eb0bb1c23": {},
},
},
}
runPatTest(t, cases, FindClientIdMatches)
}
================================================
FILE: pkg/detectors/azure_entra/refreshtoken/refreshtoken.go
================================================
package refreshtoken
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/golang-jwt/jwt/v5"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_entra"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ interface {
detectors.Detector
detectors.MaxSecretSizeProvider
detectors.StartOffsetProvider
} = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
refreshTokenPat = regexp.MustCompile(`\b[01]\.A[\w-]{50,}(?:\.\d)?\.Ag[\w-]{250,}(?:\.A[\w-]{200,})?`)
)
// 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{"0.A", "1.A"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureRefreshToken
}
func (s Scanner) Description() string {
return "Azure Entra ID refresh tokens provide long-lasting access to an account."
}
func (Scanner) MaxSecretSize() int64 { return 2048 }
func (Scanner) StartOffset() int64 { return 4096 }
// FromData will find and optionally verify Azure RefreshToken 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)
tokenMatches := findTokenMatches(dataStr)
if len(tokenMatches) == 0 {
return
}
clientMatches := azure_entra.FindClientIdMatches(dataStr)
if len(clientMatches) == 0 {
clientMatches[defaultClientId] = struct{}{}
}
tenantMatches := azure_entra.FindTenantIdMatches(dataStr)
if len(tenantMatches) == 0 {
tenantMatches[defaultTenantId] = struct{}{}
}
return s.processMatches(ctx, tokenMatches, clientMatches, tenantMatches, verify), err
}
func (s Scanner) processMatches(ctx context.Context, refreshTokens, clientIds, tenantIds map[string]struct{}, verify bool) (results []detectors.Result) {
logCtx := logContext.AddLogger(ctx)
invalidClientsForTenant := make(map[string]map[string]struct{})
validTenants := make(map[string]struct{})
TokenLoop:
for token := range refreshTokens {
var (
r *detectors.Result
clientId string
tenantId string
)
ClientLoop:
for cId := range clientIds {
clientId = cId
for tId := range tenantIds {
tenantId = tId
// Skip known invalid tenants.
invalidClients := invalidClientsForTenant[tenantId]
if invalidClients == nil {
invalidClients = map[string]struct{}{}
invalidClientsForTenant[tenantId] = invalidClients
}
if _, ok := invalidClients[clientId]; ok {
continue
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
if _, ok := validTenants[tenantId]; !ok {
if azure_entra.TenantExists(logCtx, client, tenantId) {
validTenants[tenantId] = struct{}{}
} else {
delete(tenantIds, tenantId)
continue
}
}
isVerified, extraData, verificationErr := verifyMatch(ctx, client, token, clientId, tenantId)
// Handle errors.
if verificationErr != nil {
if errors.Is(verificationErr, ErrTenantNotFound) {
// Tenant doesn't exist. This shouldn't happen with the check above.
delete(tenantIds, tenantId)
continue
} else if errors.Is(verificationErr, ErrClientNotFoundInTenant) {
// Tenant is valid but the ClientID doesn't exist.
invalidClients[clientId] = struct{}{}
continue
} else if errors.Is(verificationErr, ErrTokenExpired) {
continue TokenLoop
} else {
// Received an unexpected/unhandled error type.
r = createResult(token, clientId, tenantId, isVerified, extraData, verificationErr)
break ClientLoop
}
}
// The result is verified or there's only one associated client and tenant.
if isVerified {
r = createResult(token, clientId, tenantId, isVerified, extraData, verificationErr)
break ClientLoop
}
}
}
}
if r == nil {
// Only include the clientId and tenantId if we're confident which one it is.
if len(clientIds) != 1 || clientId == defaultClientId {
clientId = ""
}
if len(tenantIds) != 1 || tenantId == defaultTenantId {
tenantId = ""
}
r = createResult(token, clientId, tenantId, false, nil, nil)
}
results = append(results, *r)
}
return results
}
const defaultTenantId = "common"
const defaultClientId = "d3590ed6-52b3-4102-aeff-aad2292ab01c" // Microsoft Office
var (
ErrTokenExpired = errors.New("token expired")
ErrTenantNotFound = errors.New("tenant not found")
ErrClientNotFoundInTenant = errors.New("application was not found in tenant")
)
// https://learn.microsoft.com/en-us/advertising/guides/authentication-oauth-get-tokens?view=bingads-13#refresh-accesstoken
func verifyMatch(ctx context.Context, client *http.Client, refreshToken string, clientId string, tenantId string) (bool, map[string]string, error) {
data := url.Values{}
data.Set("client_id", clientId)
data.Set("scope", "https://graph.microsoft.com/.default")
data.Set("refresh_token", refreshToken)
data.Set("grant_type", "refresh_token")
tokenUrl := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantId)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenUrl, bytes.NewBufferString(data.Encode()))
if err != nil {
return false, nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
res, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
// Refresh token is valid.
if res.StatusCode == http.StatusOK {
var okResp successResponse
if err := json.NewDecoder(res.Body).Decode(&okResp); err != nil {
return false, nil, err
}
extraData := map[string]string{
"Tenant": tenantId,
"Client": clientId,
"Scope": okResp.Scope,
}
// Add claims from the access token.
token, _ := jwt.Parse(okResp.AccessToken, nil)
if token != nil {
claims := token.Claims.(jwt.MapClaims)
if app := fmt.Sprint(claims["app_displayname"]); app != "" {
extraData["Application"] = app
}
// The user information can be in a few claims.
switch {
case claims["email"] != nil:
extraData["User"] = fmt.Sprint(claims["email"])
case claims["upn"] != nil:
extraData["User"] = fmt.Sprint(claims["upn"])
case claims["unique_name"]:
extraData["User"] = fmt.Sprint(claims["unique_name"])
}
}
return true, extraData, nil
}
// Credentials *probably* aren't valid.
var errResp errorResponse
if err := json.NewDecoder(res.Body).Decode(&errResp); err != nil {
return false, nil, err
}
switch res.StatusCode {
case http.StatusBadRequest:
// Error codes can be looked up by removing the `AADSTS` prefix.
// https://login.microsoftonline.com/error?code=9002313
d := errResp.Description
switch {
case strings.HasPrefix(d, "AADSTS70008:"),
strings.HasPrefix(d, "AADSTS700082:"),
strings.HasPrefix(d, "AADSTS70043:"):
// https://login.microsoftonline.com/error?code=70008
// https://login.microsoftonline.com/error?code=700082
// https://login.microsoftonline.com/error?code=70043
return false, nil, ErrTokenExpired
case strings.HasPrefix(d, "AADSTS700016:"):
// https://login.microsoftonline.com/error?code=700016
return false, nil, ErrClientNotFoundInTenant
case strings.HasPrefix(d, "AADSTS90002:"):
// https://login.microsoftonline.com/error?code=90002
return false, nil, ErrTenantNotFound
case strings.HasPrefix(d, "AADSTS9002313:"):
// This seems to be a generic "invalid token" error code.
// 'invalid_grant': AADSTS9002313: Invalid request. Request is malformed or invalid.
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected error '%s': %s", errResp.Error, errResp.Description)
}
case http.StatusUnauthorized:
// The secret is determinately not verified (nothing to do)
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
type successResponse struct {
Scope string `json:"scope"`
AccessToken string `json:"access_token"`
}
type errorResponse struct {
Error string `json:"error"`
Description string `json:"error_description"`
}
// region Helper methods.
func findTokenMatches(data string) map[string]struct{} {
uniqueMatches := make(map[string]struct{})
for _, match := range refreshTokenPat.FindAllStringSubmatch(data, -1) {
m := match[0]
if detectors.StringShannonEntropy(m) < 4 {
continue
}
uniqueMatches[m] = struct{}{}
}
return uniqueMatches
}
func createResult(refreshToken, clientId, tenantId string, verified bool, extraData map[string]string, err error) *detectors.Result {
r := &detectors.Result{
DetectorType: detectorspb.DetectorType_AzureRefreshToken,
Raw: []byte(refreshToken),
ExtraData: extraData,
Verified: verified,
}
r.SetVerificationError(err, refreshToken)
if clientId != "" && tenantId != "" {
var sb strings.Builder
sb.WriteString(`{`)
sb.WriteString(`"refreshToken":"` + refreshToken + `"`)
sb.WriteString(`,"clientId":"` + clientId + `"`)
sb.WriteString(`,"tenantId":"` + tenantId + `"`)
sb.WriteString(`}`)
r.RawV2 = []byte(sb.String())
}
return r
}
// endregion
================================================
FILE: pkg/detectors/azure_entra/refreshtoken/refreshtoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package refreshtoken
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/detectorspb"
)
func TestRefreshToken_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("AZUREREFRESHTOKEN")
inactiveSecret := testSecrets.MustGetField("AZUREREFRESHTOKEN_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azurerefreshtoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureRefreshToken,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azurerefreshtoken secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureRefreshToken,
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 azurerefreshtoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureRefreshToken,
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 azurerefreshtoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureRefreshToken,
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("Azurerefreshtoken.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Azurerefreshtoken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azure_entra/refreshtoken/refreshtoken_test.go
================================================
package refreshtoken
import (
"context"
"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"
)
func TestRefreshToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
// Valid - 0.
{
name: "valid - token only",
input: `"refresh_token": "0.AXEAFN5Pl6TDG0ibA8_OGCw6B-kFbFJoXnhBqmJD9wukrpZxAMc.AgABAAAAAAD--DLA3VO7QrddgJg7WevrAgDs_wQA9P9g0VCdz8smoWqJBpit_3P_ntszmbCH2-dGwpsamwQMbLl7QBa7tlfXH_NtpD1vNTGkacraUMyTM5lfg1AR1DLAxs-pNSpg8NfrHbNSRAIacCpOyqtU05Dg9l5LC7ZYwxT35dQWEK0EExLER-wxjW9DrDZNQV4J3Ktv1Z4ANT2N2rqAjPYqHTDPCCcOi980ptizeImgVYiVr37Ff0Hnr_lAi4Em0wGB7KDdu319sV9Sebe91FIRDs7GVvvv7GFvKjTeXJwHCpbhdqX4X2TRMryNrTNZ8QY7_Wa25MQm7v0qfFqDW_pRMxxohGhClSedZFnkzrreIhZ8ULJ9NCf8YENRHDP3LuOJP5gex-H0MUNsJQLxlDq3bH-i7Fz_cTEB3UN_bvgE9aNe-5gal-ykO_gSx-Kk5D-vZWpLDrFUdRSGYHmKr1zgEZvQjsFUj8pGWgUwssqN9SOPxTYIEzQaxPAul5AFKcxGYt2l4Kvhh58txUdayFAglWrkx1lrxnpIcjoRmHOo45AKlgH30bVOjjltwvD4L9SGMAHhni3F6mCB6aNLGpYCHjrbdsiWolHKV0leJmBYl2Ye4eosQf9YYdgPAbCQKqOJ6gfrxJJTcfrISqDVw1c6C9qPPdHbvdol_KfdJntyfuPpHovx7AfARBcjb6nMgYRBI0wFWsGuTNDcylicMFRcZx6v283wBv4U_0PrG1_Yd5ktfgaTVXF733C-ma_-s49tAvtDrJz2bmNFpotLyyQmwOiApLjeWFkH8EjBsBtpjhzzCIrOHuHR1I1gHChDMMDxfFT2k8dqxkvBpMLZ3zFWyJNl3LYbjgy9BkTIngvpQMSgRMl_VZ2eN_fZWk5wVOHjiUJJ9n4Y8IKQRM731vK_XEaK_BdtNLfC1Gw8hfLrIZpC6152zj6RhPn03gOK7G4RL6S21IfWKrw4kl6rdaPLgmMxlaI"`,
want: []string{"0.AXEAFN5Pl6TDG0ibA8_OGCw6B-kFbFJoXnhBqmJD9wukrpZxAMc.AgABAAAAAAD--DLA3VO7QrddgJg7WevrAgDs_wQA9P9g0VCdz8smoWqJBpit_3P_ntszmbCH2-dGwpsamwQMbLl7QBa7tlfXH_NtpD1vNTGkacraUMyTM5lfg1AR1DLAxs-pNSpg8NfrHbNSRAIacCpOyqtU05Dg9l5LC7ZYwxT35dQWEK0EExLER-wxjW9DrDZNQV4J3Ktv1Z4ANT2N2rqAjPYqHTDPCCcOi980ptizeImgVYiVr37Ff0Hnr_lAi4Em0wGB7KDdu319sV9Sebe91FIRDs7GVvvv7GFvKjTeXJwHCpbhdqX4X2TRMryNrTNZ8QY7_Wa25MQm7v0qfFqDW_pRMxxohGhClSedZFnkzrreIhZ8ULJ9NCf8YENRHDP3LuOJP5gex-H0MUNsJQLxlDq3bH-i7Fz_cTEB3UN_bvgE9aNe-5gal-ykO_gSx-Kk5D-vZWpLDrFUdRSGYHmKr1zgEZvQjsFUj8pGWgUwssqN9SOPxTYIEzQaxPAul5AFKcxGYt2l4Kvhh58txUdayFAglWrkx1lrxnpIcjoRmHOo45AKlgH30bVOjjltwvD4L9SGMAHhni3F6mCB6aNLGpYCHjrbdsiWolHKV0leJmBYl2Ye4eosQf9YYdgPAbCQKqOJ6gfrxJJTcfrISqDVw1c6C9qPPdHbvdol_KfdJntyfuPpHovx7AfARBcjb6nMgYRBI0wFWsGuTNDcylicMFRcZx6v283wBv4U_0PrG1_Yd5ktfgaTVXF733C-ma_-s49tAvtDrJz2bmNFpotLyyQmwOiApLjeWFkH8EjBsBtpjhzzCIrOHuHR1I1gHChDMMDxfFT2k8dqxkvBpMLZ3zFWyJNl3LYbjgy9BkTIngvpQMSgRMl_VZ2eN_fZWk5wVOHjiUJJ9n4Y8IKQRM731vK_XEaK_BdtNLfC1Gw8hfLrIZpC6152zj6RhPn03gOK7G4RL6S21IfWKrw4kl6rdaPLgmMxlaI"},
},
{
name: "valid - token+client+tenant",
input: `
{
"tokenType": "Bearer",
"expiresIn": 4742,
"expiresOn": "2024-06-07 09:09:22.294640",
"resource": "https://graph.windows.net",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
"refreshToken": "0.AUUAMe_N-B6jSkuT5F9XHpElWlj2JcxuFFnRLm_3awiSnuJQsa1.AgABAwEAAADnfolhJpSnRYB1SVj-Hgd8Agrf-wUA9P9oElBtlKe8a-5_1t2eEmBef50SCv8exOOrgjUFMLtPQj_XH1rq3Onj2dCFQaHzhm7DfoOxj5LH4kR9jPIbPf2yRI0CgxFLEGMf0biO9LxmvVwb_NKTScIc_MK4eBsXG-En_e3vaIJS5t-ghSvPAKzl3pxiYVvBdP1i_nUHPl4dsCkk9SKCexWnhi4tg9xVVIi-MIkGDJxThmuKfAko1VHMgx-tsHRKgPoXlJi51uNO0KQQUxnDnjiWmLapCe3hVtjfoINBlb3CpiHkfW5G9dzF4cmFOQJQG9RdW-CU6t4VmlamK9gSbNYfyd7fWr7Ebv9Bo06eWEwEBpQmJONJERNScnqMs5Ztba9kUHchXqJd9wZMH-NtWejuR92IqMmPoaY4DP52Yodu2hWZPv0pFEFsthPJ3YpViOaJnCoSQ7ba-qzVr8TnvFlkI8EfFKNbl47_WncwKXDrPk2FlZwG4ywX7s0dXYvXDJ-rMQHsDcJDMABQXrxaU0Z7ozCk_ftVgBQocWZHAkzBtWZNw9dS4ltux0GeAYekUjzE7UYrPw41DLWOLrr7V-kx5sZ6h66iiTi-zdsJ28LnRIX4aZ6IC7jxIG0FK-roPldOEjy0XJ-V6QmyjkEYT3PK23vUTHIz3EQ8JqGNJMJO5mWwbedlIl2xq-0CczybkR2MJgr4UAQKUBFMYuUYGWrVygte9d48usQ6-MhAavmkyZb5Mo_PeMnnNef-cl6c8RUzMAOpeiumFEG-gTzyDgaoM1eFjtYKTz0mr-0lPfrEavE4LfGXh87oDb0lNrbbkMNhAXjz2rJW8ex1REfeBH4oit0WeMWH-sIvpT3H8jsYIawfPp7rBN9z_TMX9AUbqROEY2Nv1jSJsXCX0sjLRweYiQnl-hHFfLcWwFIFjMfs7eOKSiOBKB3ZqjQw_A8OVDxhAQJybiVgW8U41IAjXGX0DNilrmE0PhDAqs5jQIBSO66G05yJj1RY3b2z8cYMG1lKAZ10IIDfo8f3FU-_m-w6zNVVkNZko89bX8tA91EjXpoUvmnPZKT84Qx9KvtRM561ABVEYnE152821Xy0HeObVue6M5WlF0puvqk1HnkfAUDxMk6qO1Xy7o0myTIV1R2yxFPpQX_pwCRB1IutSqz0s6E1XyfbRyv8TKxjX3_tGgvUy8KrZFeYJ9pRFsKIN_AJ9_a2GMG6h1b9aCIaA7jGlOkYlC-4LnhqoKxs4RpJJIpWWN6wZstGmIACwJS4",
"familyName": "Doe",
"givenName": "John",
"identityProvider": "live.com",
"tenantId": "16515984-9303-47f6-a59f-917611c8cb2b",
"userId": "john.doe@outlook.com",
"isUserIdDisplayable": true,
"isMRRT": true,
"_clientId": "1b730954-1685-4b74-9bfd-dac224a7b894",
"_authority": "https://login.microsoftonline.com/16515984-9303-47f6-a59f-917611c8cb2b"
}`,
want: []string{`{"refreshToken":"0.AUUAMe_N-B6jSkuT5F9XHpElWlj2JcxuFFnRLm_3awiSnuJQsa1.AgABAwEAAADnfolhJpSnRYB1SVj-Hgd8Agrf-wUA9P9oElBtlKe8a-5_1t2eEmBef50SCv8exOOrgjUFMLtPQj_XH1rq3Onj2dCFQaHzhm7DfoOxj5LH4kR9jPIbPf2yRI0CgxFLEGMf0biO9LxmvVwb_NKTScIc_MK4eBsXG-En_e3vaIJS5t-ghSvPAKzl3pxiYVvBdP1i_nUHPl4dsCkk9SKCexWnhi4tg9xVVIi-MIkGDJxThmuKfAko1VHMgx-tsHRKgPoXlJi51uNO0KQQUxnDnjiWmLapCe3hVtjfoINBlb3CpiHkfW5G9dzF4cmFOQJQG9RdW-CU6t4VmlamK9gSbNYfyd7fWr7Ebv9Bo06eWEwEBpQmJONJERNScnqMs5Ztba9kUHchXqJd9wZMH-NtWejuR92IqMmPoaY4DP52Yodu2hWZPv0pFEFsthPJ3YpViOaJnCoSQ7ba-qzVr8TnvFlkI8EfFKNbl47_WncwKXDrPk2FlZwG4ywX7s0dXYvXDJ-rMQHsDcJDMABQXrxaU0Z7ozCk_ftVgBQocWZHAkzBtWZNw9dS4ltux0GeAYekUjzE7UYrPw41DLWOLrr7V-kx5sZ6h66iiTi-zdsJ28LnRIX4aZ6IC7jxIG0FK-roPldOEjy0XJ-V6QmyjkEYT3PK23vUTHIz3EQ8JqGNJMJO5mWwbedlIl2xq-0CczybkR2MJgr4UAQKUBFMYuUYGWrVygte9d48usQ6-MhAavmkyZb5Mo_PeMnnNef-cl6c8RUzMAOpeiumFEG-gTzyDgaoM1eFjtYKTz0mr-0lPfrEavE4LfGXh87oDb0lNrbbkMNhAXjz2rJW8ex1REfeBH4oit0WeMWH-sIvpT3H8jsYIawfPp7rBN9z_TMX9AUbqROEY2Nv1jSJsXCX0sjLRweYiQnl-hHFfLcWwFIFjMfs7eOKSiOBKB3ZqjQw_A8OVDxhAQJybiVgW8U41IAjXGX0DNilrmE0PhDAqs5jQIBSO66G05yJj1RY3b2z8cYMG1lKAZ10IIDfo8f3FU-_m-w6zNVVkNZko89bX8tA91EjXpoUvmnPZKT84Qx9KvtRM561ABVEYnE152821Xy0HeObVue6M5WlF0puvqk1HnkfAUDxMk6qO1Xy7o0myTIV1R2yxFPpQX_pwCRB1IutSqz0s6E1XyfbRyv8TKxjX3_tGgvUy8KrZFeYJ9pRFsKIN_AJ9_a2GMG6h1b9aCIaA7jGlOkYlC-4LnhqoKxs4RpJJIpWWN6wZstGmIACwJS4","clientId":"1b730954-1685-4b74-9bfd-dac224a7b894","tenantId":"16515984-9303-47f6-a59f-917611c8cb2b"}`},
},
{
name: "valid - 0. in README",
input: `
### Connection settings
The connection settings are defined in the automation variables.
1. Create the following [user defined variables](https://docs.helloid.com/hc/en-us/articles/360014169933-How-to-Create-and-Manage-User-Defined-Variables)
| Variable name | Description | Example value |
| ----------------- | ------------------------------------------------------------- | ------------------------------------- |
| AFASBaseUri | Base URI of the AFAS REST API endpoint for this environment | https://12345.rest.afas.online/ProfitRestServices |
| AFASToke | App token in XML format for this environment | \\1\\D5R324DD5F4TRD945E530ED3CDD70D94BBDEC4C732B43F285ECB12345678\\ |
| AADtenantID | Id of the Azure tenant | 12fc345b-0c67-4cde-8902-dabf2cad34b5 |
| AADAppId | Id of the Azure app | f12345c6-7890-1f23-b456-789eb0bb1c23 |
| AADRefreshToken | Refresh token of the Azure app | 0.ABCDEFGHIJKLMNOPQRS_PK0mtsE5afl5BYdPsASFbrS7jIZ0AAc.AgABAAAAAAD--DLA3VO7QrddgJg7WevrAgDs_wQA9P-XOTtPMo2xp9vfbHGvVkHaBZh4D3YmTkx_WagBOk358QjDwHUsiuVvyKvP6FTbQQt8kCidfMC9cmIYesHG4Ft2B1HwJNX28OpiFPuFti1D4Is30GgQ685i_ovS4iXDCUgtm2zpI6ZQJVqoOidXZQW_lSupdcclMK_JCIb7LBuJBDXfy0-f75C734_nxL0nggS9mn-e_KuJpHvypvU8OS9MPDBArhUopZum2y-2oNE65Wr-xpKm_Zeyr3iUGSZg98nbaryHw-lbeyFC8LcNqqMB_T7BcgvJicHSnj6DtjjpMyjKMwsCAnxz2bUYoLLjGFHk8EhDUCuV9lzUW1BTko5_I31TQdX0XY94vHTU34N93t3QPrQFMf8UhDjfQKiCDj3r2b7YR9ndS8MNp9MIa1CbL8vI4EM8GO4wtVI30Dhca4HaMtpph6uJp3echt-q7AVNQ_7ZHgx_YFZNqDmJyYq3nrae7LYRo0kvM382ss7JpCylodwya89mC_SlnrFhLM_zbt1TQkOtZqiVHbdQk3z-MX1iZso5Mk17Yks1ao0mS0RJfWVWSlOq_Sp-2yaiCsP-lV1PVdvvY_AkuOulP1kPG_VfC0DN3pGjSQJ8J9Ot5hfyElWyPst9Nc-ODErLhEqIl-3IR6wPKFN2ffjt8-dtCVMlVdBd1QANQOFBiIGA-_BZdGLvzROrWCOE9dDtyBQ_LnxdnnOVdjUqJ-xdql1p13Xjy6ZTtcZtTDmFN5hSMffYuUtuwEOy_Xb91Y2tvwOxcSe9dj7ElOLZDo2C7fGsMgaIJ1gK8xt9OWsS1o1sQZKQADTZq5TTxJp7PY3tJsUnOlD4q8ZEyVBQAvRKinpajBRcbq2lTCVt0JgXAryWztqYTpAxiqaBr51vuR4pbVRtKv-h_10tYD-TUV1WeX2fY3GuZA4B5g |
## contents`,
want: []string{`{"refreshToken":"0.ABCDEFGHIJKLMNOPQRS_PK0mtsE5afl5BYdPsASFbrS7jIZ0AAc.AgABAAAAAAD--DLA3VO7QrddgJg7WevrAgDs_wQA9P-XOTtPMo2xp9vfbHGvVkHaBZh4D3YmTkx_WagBOk358QjDwHUsiuVvyKvP6FTbQQt8kCidfMC9cmIYesHG4Ft2B1HwJNX28OpiFPuFti1D4Is30GgQ685i_ovS4iXDCUgtm2zpI6ZQJVqoOidXZQW_lSupdcclMK_JCIb7LBuJBDXfy0-f75C734_nxL0nggS9mn-e_KuJpHvypvU8OS9MPDBArhUopZum2y-2oNE65Wr-xpKm_Zeyr3iUGSZg98nbaryHw-lbeyFC8LcNqqMB_T7BcgvJicHSnj6DtjjpMyjKMwsCAnxz2bUYoLLjGFHk8EhDUCuV9lzUW1BTko5_I31TQdX0XY94vHTU34N93t3QPrQFMf8UhDjfQKiCDj3r2b7YR9ndS8MNp9MIa1CbL8vI4EM8GO4wtVI30Dhca4HaMtpph6uJp3echt-q7AVNQ_7ZHgx_YFZNqDmJyYq3nrae7LYRo0kvM382ss7JpCylodwya89mC_SlnrFhLM_zbt1TQkOtZqiVHbdQk3z-MX1iZso5Mk17Yks1ao0mS0RJfWVWSlOq_Sp-2yaiCsP-lV1PVdvvY_AkuOulP1kPG_VfC0DN3pGjSQJ8J9Ot5hfyElWyPst9Nc-ODErLhEqIl-3IR6wPKFN2ffjt8-dtCVMlVdBd1QANQOFBiIGA-_BZdGLvzROrWCOE9dDtyBQ_LnxdnnOVdjUqJ-xdql1p13Xjy6ZTtcZtTDmFN5hSMffYuUtuwEOy_Xb91Y2tvwOxcSe9dj7ElOLZDo2C7fGsMgaIJ1gK8xt9OWsS1o1sQZKQADTZq5TTxJp7PY3tJsUnOlD4q8ZEyVBQAvRKinpajBRcbq2lTCVt0JgXAryWztqYTpAxiqaBr51vuR4pbVRtKv-h_10tYD-TUV1WeX2fY3GuZA4B5g","clientId":"f12345c6-7890-1f23-b456-789eb0bb1c23","tenantId":"12fc345b-0c67-4cde-8902-dabf2cad34b5"}`},
},
// Valid 1.
{
name: "valid - 1. token only",
input: ` "refresh_token": "1.AVEAPn9m_nUaQ0iPPuqFsWAkYjIyPGgTIDFBgPvLEoUSLQVRAG5RAA.AgABAwEAAADW6jl31mB3T7ugrWTT8pFeAwDj_wUA9P8ZsxEzkXInsWHkCylMQMSSKto-NoegPmNj0uIemgAvxjnsDVGpC7sDRl4oEd51nQLQYowQYQ8aEcHh3nRrACc37UPYN-bwDte-tiwOEKuuGTOUrZft6YCqYiBoj7p3GZvKkIkUOGZvx7nydI1WoH9c7Z62NstZJ7ju_V38t5He6cKXEzNtlnrHpctxJX1uxxizdvwIR-_2VyMQjSSJS5lOS0Hi4Z_Nlthos5G-Gb-h9Y96fkkVm0D5E4xQh9avS7eCAPE2-N_guF3tmm7B4aqJg1lGnwv3WDWim14QhkF6Aji7juJUNmAExFyBaM7WnV_u3JnT-UNCz1p0O3AHa9d-dyDTUxQ8m_riB1HPoZZo6wPxg6txs6-fUE4LDR6tB5b43zwUl9XufcL4gKwnheLr8LvpJGjJn2tZUQzoU-ow4AZtJIxblfgYU_Zq0WOPJXltgAEw2JVoGsRy2jX8mXFZq1iCK5uEKBPXgrEfV-simUqI8GRZgXA1EnxG950MuaVfP3ZpsTYPGsvQgSzsUBKSy7cLd0p7UYtLub9UpX2PJxHrLQjACF-CSOMatVfSNzTErhSEmVWndpt87Yhova-XJUV48UxQ4ZZz26G6nOQ9qJ6db8ReAzBnok10e0eBuHR6K0OzcO54gjiQWPR4Tur7hD82KmYdOtShz234hDRGuS_b7mThfr_2ef9b2TQ9XYEV2QDUWiFYplfU0kOKA-wA7jOJGhXDkaJCIURxy53KuZPolXjTAy4",
"expires_at": 1733138350.558087
}`,
want: []string{"1.AVEAPn9m_nUaQ0iPPuqFsWAkYjIyPGgTIDFBgPvLEoUSLQVRAG5RAA.AgABAwEAAADW6jl31mB3T7ugrWTT8pFeAwDj_wUA9P8ZsxEzkXInsWHkCylMQMSSKto-NoegPmNj0uIemgAvxjnsDVGpC7sDRl4oEd51nQLQYowQYQ8aEcHh3nRrACc37UPYN-bwDte-tiwOEKuuGTOUrZft6YCqYiBoj7p3GZvKkIkUOGZvx7nydI1WoH9c7Z62NstZJ7ju_V38t5He6cKXEzNtlnrHpctxJX1uxxizdvwIR-_2VyMQjSSJS5lOS0Hi4Z_Nlthos5G-Gb-h9Y96fkkVm0D5E4xQh9avS7eCAPE2-N_guF3tmm7B4aqJg1lGnwv3WDWim14QhkF6Aji7juJUNmAExFyBaM7WnV_u3JnT-UNCz1p0O3AHa9d-dyDTUxQ8m_riB1HPoZZo6wPxg6txs6-fUE4LDR6tB5b43zwUl9XufcL4gKwnheLr8LvpJGjJn2tZUQzoU-ow4AZtJIxblfgYU_Zq0WOPJXltgAEw2JVoGsRy2jX8mXFZq1iCK5uEKBPXgrEfV-simUqI8GRZgXA1EnxG950MuaVfP3ZpsTYPGsvQgSzsUBKSy7cLd0p7UYtLub9UpX2PJxHrLQjACF-CSOMatVfSNzTErhSEmVWndpt87Yhova-XJUV48UxQ4ZZz26G6nOQ9qJ6db8ReAzBnok10e0eBuHR6K0OzcO54gjiQWPR4Tur7hD82KmYdOtShz234hDRGuS_b7mThfr_2ef9b2TQ9XYEV2QDUWiFYplfU0kOKA-wA7jOJGhXDkaJCIURxy53KuZPolXjTAy4"},
},
{
name: "valid - 1. tenant+client+token",
input: `
async function getAccessToken() {
const tenantId = "31d1b7f4-4c4c-44cf-8d4e-b63e8512543e";
const clientId = "16ed71fb-067e-47d9-b4bc-7656b14f1c5e";
const clientSecret = ""; //para que funcione en sus ambientes tienen que poner el secreto,
//si no lo tienen me lo piden y se los comparto por whatsapp,
//lo tuvé que quitar porque no me dejaba hacer commit de los cambios en el repositorio
const scope = "https://analysis.windows.net/powerbi/api/.default";
let refresh_token = "1.AWEBqY9dsQppikubCN8WQsbFVyBfrV_ioNtAn7uoXAmQmkRiAUthAQ.AgABAwEAAADW6jl31mB3T7ugrWTT8pFeAwDs_yUA9P8OThUk9d3XIZbW4OGsJwHqqvjnVfcvEH4nejPU6R6-3onU34aSbVTEmxec0Nn3PaKfTBxucT-bu5XLSaTZSePKAAZw22RpqBb1w6ySb5GvvcCVpFU45mNfX5OH63y2Ryt-B7Beyp5yzlIgVgQA2S4OKhd_2qoVQoQXLApTwR78awwMFEQ7eVSbu5DO52dxisjB9ApHmpDCBip5y2MzyS7TizR31e-qBTnCMWt9RuHcKJySFFa-yPRBqYCgZLQWmEsKXBq-RIJToFsaGhVH2sXGXec0-Qsd9CvSPNFfGUDb_d2FLkZyKYKPra7Wmsvpw6qZJxO_TYprs1TbeWJYTTWT6WWI3xn10XtVml0a0P77ESqAWs-nbl6fS15mE24ZVU6rsuD7Q5AmtFfaddVN-JFP3fJ-6VsiY3KAefmdNULF_AVfMxAelBDSHtNllsMv4Qqs8N4h5bY4cabHibpu_OVA7WzfkNbxQ1dZpccZ9pi--xq5BCU3QAzereqYwmKretykB8twHw8Ryl5UVGocBNSJD65w2K3FJGZ6zbinfb_g1vV39iFxLdUz3JT1obce5ndeMBUeFmhN5XsczKAzTRK9c8aX6sdOd5pw5vUe-98qFRypPvCSF4hVA2ziwH38V9Dtc56UEVSMKISOacRMs8F_m9XtxP4X5KsWICIrK8_EXWfgmvEQnXm5PHV24ROsbnmmtUJWN1-vgzmNmSQ54_66W-fsCdnYAzDlwZeKr7wTZYO82nepNHX-wvTTEPV-QlrTPQFAlguP6nnxRc8MoxyiEvT4fOsDwD4yWFkLMMlKbyB6pQF_0CW_rQbyl0e6EKP2HbIDVKj628MDizjsdX693gplJevjF5g";
`,
want: []string{`{"refreshToken":"1.AWEBqY9dsQppikubCN8WQsbFVyBfrV_ioNtAn7uoXAmQmkRiAUthAQ.AgABAwEAAADW6jl31mB3T7ugrWTT8pFeAwDs_yUA9P8OThUk9d3XIZbW4OGsJwHqqvjnVfcvEH4nejPU6R6-3onU34aSbVTEmxec0Nn3PaKfTBxucT-bu5XLSaTZSePKAAZw22RpqBb1w6ySb5GvvcCVpFU45mNfX5OH63y2Ryt-B7Beyp5yzlIgVgQA2S4OKhd_2qoVQoQXLApTwR78awwMFEQ7eVSbu5DO52dxisjB9ApHmpDCBip5y2MzyS7TizR31e-qBTnCMWt9RuHcKJySFFa-yPRBqYCgZLQWmEsKXBq-RIJToFsaGhVH2sXGXec0-Qsd9CvSPNFfGUDb_d2FLkZyKYKPra7Wmsvpw6qZJxO_TYprs1TbeWJYTTWT6WWI3xn10XtVml0a0P77ESqAWs-nbl6fS15mE24ZVU6rsuD7Q5AmtFfaddVN-JFP3fJ-6VsiY3KAefmdNULF_AVfMxAelBDSHtNllsMv4Qqs8N4h5bY4cabHibpu_OVA7WzfkNbxQ1dZpccZ9pi--xq5BCU3QAzereqYwmKretykB8twHw8Ryl5UVGocBNSJD65w2K3FJGZ6zbinfb_g1vV39iFxLdUz3JT1obce5ndeMBUeFmhN5XsczKAzTRK9c8aX6sdOd5pw5vUe-98qFRypPvCSF4hVA2ziwH38V9Dtc56UEVSMKISOacRMs8F_m9XtxP4X5KsWICIrK8_EXWfgmvEQnXm5PHV24ROsbnmmtUJWN1-vgzmNmSQ54_66W-fsCdnYAzDlwZeKr7wTZYO82nepNHX-wvTTEPV-QlrTPQFAlguP6nnxRc8MoxyiEvT4fOsDwD4yWFkLMMlKbyB6pQF_0CW_rQbyl0e6EKP2HbIDVKj628MDizjsdX693gplJevjF5g","clientId":"16ed71fb-067e-47d9-b4bc-7656b14f1c5e","tenantId":"31d1b7f4-4c4c-44cf-8d4e-b63e8512543e"}`},
},
{
name: "valid - 1. with more than 3 segments",
input: `- request:
body: client_id=04b07795-8ddb-461a-bbee-02f9e1bf7b46&grant_type=refresh_token&client_info=1&claims=%7B%22access_token%22%3A+%7B%22xms_cc%22%3A+%7B%22values%22%3A+%5B%22CP1%22%5D%7D%7D%7D&refresh_token=1.AAEA-W8xnNOnEke-ljgE71hsE5V3sATbjRpGu-4G-eG_e0YBAFIaAA.1.AgABAwEAAAAuQLDzsjJ3TYwhxABdnzRyAwDs_wUA7_9ENX2x1IM0b4hPzM-Ba_-qsHQqGxLKdo8wXF8BKQjnNc3wrqvP54z75uPEWb9uNOqw_Y8oxEQHggfkdIiq1NjPeA-A9jR2AI28nwlPd8dyuglTrUhLEKCKH0UFCeOi0lSxr7pefIa97LSJsDFKYPg1bCd9iuyRI5zQVGFbfHfq7gI8TSbpaVRSzNlsgftBrzIH_Zk55WCWz9ln8B-K1mc8gFDKsnclyvyCQU6e4CE0_6dHq1FXD-BwwV0yC1S9yyh673EHgY47s950p3Yqc7a8fOKY7iuwNKCDML51CUAZusRWfRYx0d1FXMI-JUfoBHTaZwsQyFePTlLjxkk2iEk4v9PTlTIvBdzZ6A8BVNDvpK_lBHgEpN_HVEbWM9ZHvWbeIU2_Lwt0SqLJEnq5GkTowX3aJe36JXWE6NBp5NJWS5-0EfEtl5iIWxtNG6u2E7lGAEbvUEAGXYa0abLxNwRiKvMNCKw01v42xIw1HqonNMT-tgY08KI3Icbyv-hzEwUwY8LYcjOGQTejDRe7CM9IogLe5flpK6m5aYKF8k4qVMN2PqCGCpofcqqyS448k9ATYx1Dm4-MAVsWScb22M106yIRSIbdo7tKdr3vBdNf0_FT0I-r20iDnUw_6sQc_Q8tR9uRuZbtrwD6IBAyYzqTG2KacAG6Gac-J5p-fsnPdjy0RmurvE149oA4G0KcAatNPmreiGzArXJEx7z20QwCgrh4j11j3dLJQMMafaxPdjHjPkwrG8Vz7xHVvRlfcn6x1d2Xhyq2VB6BwdZVIukbvxSg9Ci34qlKunOtohUxvisRRryV-w6MV1BomJz3W0QM0cTm5KVWpH9_0tQrioelqwvstQ6bOHRA3r7CzTZw0lfGMoDlaPubUqiy5t6P_b40hpkt40drKKHN972GwSDeR19cYiUFIONkc5APsV17tq0XZZgB8zpL-WilYK2SBQzescd4W1yXpFuh-uZ7bLAnQaa6xZzFDkN9-v4chZ2UAAvBsIURr7Q_8N_w2nH_.AQABFAEAAAAuQLDzsjJ3TYwhxABdnzRyNxO45BG1O4-twAhtMj2ZAGVMkIFTMaFoxpzzBJ7zB99xWtRIkmYAput3pQWfY44PP3WY0mRvEqSuLWlLa79Nz8jJANXNNTbPvXt8F_BDxeZUwb7gNax-q2Fr12Gb5YnTVnq9EUU9QEcuThPgC7tFWFu3_iwKjR-IMMcnQj6C7eh-ZcPMIn5Pkb3FkLwD7aZblol-4Z18pXV7dBOO8i0i4VZ5ud7tkxL5UjDZdbM8NrogAA&scope=https%3A%2F%2Fmanagement.core.windows.net%2F%2F.default+offline_access+openid+profile
headers:`,
want: []string{`1.AAEA-W8xnNOnEke-ljgE71hsE5V3sATbjRpGu-4G-eG_e0YBAFIaAA.1.AgABAwEAAAAuQLDzsjJ3TYwhxABdnzRyAwDs_wUA7_9ENX2x1IM0b4hPzM-Ba_-qsHQqGxLKdo8wXF8BKQjnNc3wrqvP54z75uPEWb9uNOqw_Y8oxEQHggfkdIiq1NjPeA-A9jR2AI28nwlPd8dyuglTrUhLEKCKH0UFCeOi0lSxr7pefIa97LSJsDFKYPg1bCd9iuyRI5zQVGFbfHfq7gI8TSbpaVRSzNlsgftBrzIH_Zk55WCWz9ln8B-K1mc8gFDKsnclyvyCQU6e4CE0_6dHq1FXD-BwwV0yC1S9yyh673EHgY47s950p3Yqc7a8fOKY7iuwNKCDML51CUAZusRWfRYx0d1FXMI-JUfoBHTaZwsQyFePTlLjxkk2iEk4v9PTlTIvBdzZ6A8BVNDvpK_lBHgEpN_HVEbWM9ZHvWbeIU2_Lwt0SqLJEnq5GkTowX3aJe36JXWE6NBp5NJWS5-0EfEtl5iIWxtNG6u2E7lGAEbvUEAGXYa0abLxNwRiKvMNCKw01v42xIw1HqonNMT-tgY08KI3Icbyv-hzEwUwY8LYcjOGQTejDRe7CM9IogLe5flpK6m5aYKF8k4qVMN2PqCGCpofcqqyS448k9ATYx1Dm4-MAVsWScb22M106yIRSIbdo7tKdr3vBdNf0_FT0I-r20iDnUw_6sQc_Q8tR9uRuZbtrwD6IBAyYzqTG2KacAG6Gac-J5p-fsnPdjy0RmurvE149oA4G0KcAatNPmreiGzArXJEx7z20QwCgrh4j11j3dLJQMMafaxPdjHjPkwrG8Vz7xHVvRlfcn6x1d2Xhyq2VB6BwdZVIukbvxSg9Ci34qlKunOtohUxvisRRryV-w6MV1BomJz3W0QM0cTm5KVWpH9_0tQrioelqwvstQ6bOHRA3r7CzTZw0lfGMoDlaPubUqiy5t6P_b40hpkt40drKKHN972GwSDeR19cYiUFIONkc5APsV17tq0XZZgB8zpL-WilYK2SBQzescd4W1yXpFuh-uZ7bLAnQaa6xZzFDkN9-v4chZ2UAAvBsIURr7Q_8N_w2nH_.AQABFAEAAAAuQLDzsjJ3TYwhxABdnzRyNxO45BG1O4-twAhtMj2ZAGVMkIFTMaFoxpzzBJ7zB99xWtRIkmYAput3pQWfY44PP3WY0mRvEqSuLWlLa79Nz8jJANXNNTbPvXt8F_BDxeZUwb7gNax-q2Fr12Gb5YnTVnq9EUU9QEcuThPgC7tFWFu3_iwKjR-IMMcnQj6C7eh-ZcPMIn5Pkb3FkLwD7aZblol-4Z18pXV7dBOO8i0i4VZ5ud7tkxL5UjDZdbM8NrogAA`},
},
// Invalid
{
name: "invalid - too short",
input: `"refresh_token": "0.AXEAFN5Pl6TDG0ibA8_OGCw6B-kFbFJoXnhBqmJD9wukrpZxAMc.AgABAAAAAAD--DLA3VO7QrddgJg7WevrAgDs_wQA9P9g0VCdz8sm..."`,
},
{
name: "invalid - low entropy",
input: `"refresh_token": "0.Axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.Agxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/azure_entra/serviceprincipal/sp.go
================================================
package serviceprincipal
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/golang-jwt/jwt/v5"
)
var (
Description = "Azure is a cloud service offering a wide range of services including compute, analytics, storage, and networking. Azure credentials can be used to access and manage these services."
ErrConditionalAccessPolicy = errors.New("access blocked by Conditional Access policies (AADSTS53003)")
ErrSecretInvalid = errors.New("invalid client secret provided")
ErrSecretExpired = errors.New("the provided secret is expired")
ErrTenantNotFound = errors.New("tenant not found")
ErrClientNotFoundInTenant = errors.New("application was not found in tenant")
)
type TokenOkResponse struct {
AccessToken string `json:"access_token"`
}
type TokenErrResponse struct {
Error string `json:"error"`
Description string `json:"error_description"`
}
// VerifyCredentials attempts to get a token using the provided client credentials.
// See: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#get-a-token
func VerifyCredentials(ctx context.Context, client *http.Client, tenantId string, clientId string, clientSecret string) (bool, map[string]string, error) {
data := url.Values{}
data.Set("client_id", clientId)
data.Set("scope", "https://graph.microsoft.com/.default")
data.Set("client_secret", clientSecret)
data.Set("grant_type", "client_credentials")
tokenUrl := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantId)
encodedData := data.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenUrl, strings.NewReader(encodedData))
if err != nil {
return false, nil, nil
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Content-Length", strconv.Itoa(len(encodedData)))
res, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
// Credentials are valid.
if res.StatusCode == http.StatusOK {
var okResp TokenOkResponse
if err := json.NewDecoder(res.Body).Decode(&okResp); err != nil {
return false, nil, err
}
extraData := map[string]string{
"rotation_guide": "https://howtorotate.com/docs/tutorials/azure/",
"tenant": tenantId,
"client": clientId,
}
// Add claims from the access token.
if token, _ := jwt.Parse(okResp.AccessToken, nil); token != nil {
claims := token.Claims.(jwt.MapClaims)
if app := claims["app_displayname"]; app != nil {
extraData["application"] = fmt.Sprint(app)
}
}
return true, extraData, nil
}
// Credentials *probably* aren't valid.
var errResp TokenErrResponse
if err := json.NewDecoder(res.Body).Decode(&errResp); err != nil {
return false, nil, err
}
switch res.StatusCode {
case http.StatusBadRequest, http.StatusUnauthorized:
// Error codes can be looked up by removing the `AADSTS` prefix.
// https://login.microsoftonline.com/error?code=9002313
// TODO: Handle AADSTS900382 (https://github.com/Azure/azure-sdk-for-js/issues/30557)
d := errResp.Description
switch {
case strings.HasPrefix(d, "AADSTS53003:"):
return false, nil, ErrConditionalAccessPolicy
case strings.HasPrefix(d, "AADSTS700016:"):
// https://login.microsoftonline.com/error?code=700016
return false, nil, ErrClientNotFoundInTenant
case strings.HasPrefix(d, "AADSTS7000215:"):
// https://login.microsoftonline.com/error?code=7000215
return false, nil, ErrSecretInvalid
case strings.HasPrefix(d, "AADSTS7000222:"):
// The secret has expired.
// https://login.microsoftonline.com/error?code=7000222
return false, nil, ErrSecretExpired
case strings.HasPrefix(d, "AADSTS90002:"):
// https://login.microsoftonline.com/error?code=90002
return false, nil, ErrTenantNotFound
default:
return false, nil, fmt.Errorf("unexpected error '%s': %s", errResp.Error, errResp.Description)
}
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
================================================
FILE: pkg/detectors/azure_entra/serviceprincipal/v1/spv1.go
================================================
package v1
import (
"context"
"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/detectors/azure_entra"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_entra/serviceprincipal"
v2 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_entra/serviceprincipal/v2"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ interface {
detectors.Detector
detectors.Versioner
} = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// TODO: Azure storage access keys and investigate other types of creds.
// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate
// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#third-case-access-token-request-with-a-federated-credential
//clientSecretPat = regexp.MustCompile(`(?i)(?:secret|password| -p[ =]).{0,80}?([\w~@[\]:.?*/+=-]{31,34}`)
// TODO: Tighten this regex and replace it with above.
secretPat = regexp.MustCompile(`(?i)(?:secret|password| -p[ =]).{0,80}[^A-Za-z0-9!#$%&()*+,\-./:;<=>?@[\\\]^_{|}~]([A-Za-z0-9!#$%&()*+,\-./:;<=>?@[\\\]^_{|}~]{31,34})[^A-Za-z0-9!#$%&()*+,\-./:;<=>?@[\\\]^_{|}~]`)
)
func (s Scanner) Version() int {
return 1
}
// 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{"azure", "az", "entra", "msal", "login.microsoftonline.com", ".onmicrosoft.com"}
}
// FromData will find and optionally verify Azure 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)
clientSecrets := findSecretMatches(dataStr)
if len(clientSecrets) == 0 {
return
}
clientIds := azure_entra.FindClientIdMatches(dataStr)
if len(clientIds) == 0 {
return
}
tenantIds := azure_entra.FindTenantIdMatches(dataStr)
client := s.client
if client == nil {
client = defaultClient
}
// The handling logic is identical for both versions.
results = append(results, v2.ProcessData(ctx, clientSecrets, clientIds, tenantIds, verify, client)...)
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Azure
}
func (s Scanner) Description() string {
return serviceprincipal.Description
}
func findSecretMatches(data string) map[string]struct{} {
uniqueMatches := make(map[string]struct{})
for _, match := range secretPat.FindAllStringSubmatch(data, -1) {
m := match[1]
// Ignore secrets that are handled by the V2 detector.
if v2.SecretPat.MatchString(m) {
continue
}
uniqueMatches[m] = struct{}{}
}
return uniqueMatches
}
================================================
FILE: pkg/detectors/azure_entra/serviceprincipal/v1/spv1_integration_test.go
================================================
//go:build detectors
// +build detectors
package v1
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAzure_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AZURE_SECRET")
secretInactive := testSecrets.MustGetField("AZURE_INACTIVE")
id := testSecrets.MustGetField("AZURE_ID")
tenantId := testSecrets.MustGetField("AZURE_TENANT_ID")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf(`
tenant_id=%s
client_id=%s
client_secret=%s
client_secret=%s
`, tenantId, id, secretInactive, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Azure,
Redacted: id,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf(`
tenant_id=%s
client_id=%s
client_secret=%s
`, tenantId, id, secretInactive)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Azure,
Redacted: id,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Azure.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
for i := range got {
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Azure.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azure_entra/serviceprincipal/v1/spv1_test.go
================================================
package v1
import (
"testing"
"github.com/google/go-cmp/cmp"
)
type testCase struct {
Input string
Expected map[string]struct{}
}
func Test_FindClientSecretMatches(t *testing.T) {
cases := map[string]testCase{
"client_secret": {
Input: ` "TenantId": "3d7e0652-b03d-4ed2-bf86-f1299cecde17",
"ClientSecret": "gHduiL_j6t4b6DG?Qr-G6M@IOS?mX3B9",`,
Expected: map[string]struct{}{"gHduiL_j6t4b6DG?Qr-G6M@IOS?mX3B9": {}},
},
"client_secret1": {
Input: ` public static string clientId = "413ff05b-6d54-41a7-9271-9f964bc10624";
public static string clientSecret = "k72~odcN_6TbVh5D~19_1Qkj~87trteArL";
private const string `,
Expected: map[string]struct{}{"k72~odcN_6TbVh5D~19_1Qkj~87trteArL": {}},
},
"client_secret2": {
Input: ` "azClientSecret": "2bWD_tu3~9B0_.R0W3BFJN-Hu_xjfR8EL5",
"kvVaultUri": "https://corp.vault.azure.net/",`,
Expected: map[string]struct{}{"2bWD_tu3~9B0_.R0W3BFJN-Hu_xjfR8EL5": {}},
},
"client_secret3": {
Input: `# COMMAND ----------
clientID = "193e3d24-8d04-404c-95a9-074efaa83147"
tenantID = "28241a04-7ac0-44f1-a996-84dc181f9861"
secret = "a2djRWTXDS1iMbThoK.C7e:yVsUdL3[:"`,
Expected: map[string]struct{}{"a2djRWTXDS1iMbThoK.C7e:yVsUdL3[:": {}},
},
"client_secret4": {
Input: `tenantID = "9f37a392-g0ae-1280-9796-f1864210effc"
secret = "s.1_56k~5jmRDm23y.dTg5_XjTAcRjCbH."
# COMMAND ----------
configs = {"fs.azure.account.auth.type": "OAuth"`,
Expected: map[string]struct{}{"s.1_56k~5jmRDm23y.dTg5_XjTAcRjCbH.": {}},
},
"client_secret5": {
Input: `public class HardcodedAzureCredentials {
private final String clientId = "81734019-15a3-50t8-3253-5abe78abc3a1";
private final String username = "username@example.onmicrosoft.com";
private final String clientSecret = "1n1.qAc~3Q-1t38aF79Xzv5AUEfR5-ct3_";`,
Expected: map[string]struct{}{"1n1.qAc~3Q-1t38aF79Xzv5AUEfR5-ct3_": {}},
},
// https://github.com/kedacore/keda/blob/main/pkg/scalers/azure_log_analytics_scaler_test.go
"client_secret6": {
Input: `const (
tenantID = "d248da64-0e1e-4f79-b8c6-72ab7aa055eb"
clientID = "41826dd4-9e0a-4357-a5bd-a88ad771ea7d"
clientSecret = "U6DtAX5r6RPZxd~l12Ri3X8J9urt5Q-xs"
workspaceID = "074dd9f8-c368-4220-9400-acb6e80fc325"`,
Expected: map[string]struct{}{"U6DtAX5r6RPZxd~l12Ri3X8J9urt5Q-xs": {}},
},
"client_secret7": {
Input: ` "AZUREAD-AKS-APPID-SECRET": "xW25Gt-Mf0.ue3jFqE68jtFqtt-4L_8R51",
"AZUREAD-AKS-TENANTID": "d3a761f8-e7ea-473a-b907-1e7b3ef92aa9",`,
Expected: map[string]struct{}{"xW25Gt-Mf0.ue3jFqE68jtFqtt-4L_8R51": {}},
},
"client_secret8": {
Input: ` "AZUREAD-AKS-APPID-SECRET": "8w__IGsaY.6g6jUxb1.pPGK262._pgX.q-",`,
Expected: map[string]struct{}{"8w__IGsaY.6g6jUxb1.pPGK262._pgX.q-": {}},
},
// "client_secret6": {
// Input: ``,
// Expected: map[string]struct{}{"": {}},
// },
"password": {
Input: `# Login using Service Principal
$ApplicationId = "5cec5dfb-0ac4-4938-b477-3f9638881b93"
$SecuredPassword = ConvertTo-SecureString -String "gHduiL_j6t4b6DG?Qr-G6M@IOS?mX3B9" -AsPlainText -Force
$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $ApplicationId, $SecuredPassword`,
Expected: map[string]struct{}{"gHduiL_j6t4b6DG?Qr-G6M@IOS?mX3B9": {}},
},
// False positives
"placeholder_secret": {
Input: `- Log in with a service principal using a client secret:
az login --service-principal --username {{http://azure-cli-service-principal}} --password {{secret}} --tenant {{someone.onmicrosoft.com}}`,
Expected: nil,
},
// "client_secret3": {
// Input: ``,
// Expected: map[string]struct{}{
// "": {},
// },
// },
}
for name, test := range cases {
t.Run(name, func(t *testing.T) {
matches := findSecretMatches(test.Input)
if len(matches) == 0 {
if len(test.Expected) != 0 {
t.Fatalf("no matches found, expected: %v", test.Expected)
return
} else {
return
}
}
if diff := cmp.Diff(test.Expected, matches); diff != "" {
t.Errorf("%s diff: (-want +got)\n%s", name, diff)
}
})
}
}
================================================
FILE: pkg/detectors/azure_entra/serviceprincipal/v2/spv2.go
================================================
package v2
import (
"context"
"errors"
"net/http"
"regexp"
"strings"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_entra"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_entra/serviceprincipal"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ interface {
detectors.Detector
detectors.Versioner
} = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
SecretPat = regexp.MustCompile(`(?:[^a-zA-Z0-9_~.-]|\A)([a-zA-Z0-9_~.-]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:[^a-zA-Z0-9_~.-]|\z)`)
)
func (s Scanner) Version() int {
return 2
}
// 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{"q~"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Azure
}
func (s Scanner) Description() string {
return serviceprincipal.Description
}
// FromData will find and optionally verify Azure 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)
clientSecrets := findSecretMatches(dataStr)
if len(clientSecrets) == 0 {
return results, nil
}
clientIds := azure_entra.FindClientIdMatches(dataStr)
tenantIds := azure_entra.FindTenantIdMatches(dataStr)
client := s.client
if client == nil {
client = defaultClient
}
results = append(results, ProcessData(ctx, clientSecrets, clientIds, tenantIds, verify, client)...)
return results, nil
}
func ProcessData(ctx context.Context, clientSecrets, clientIds, tenantIds map[string]struct{}, verify bool, client *http.Client) (results []detectors.Result) {
logCtx := logContext.AddLogger(ctx)
invalidClientsForTenant := make(map[string]map[string]struct{})
SecretLoop:
for clientSecret := range clientSecrets {
var (
r *detectors.Result
clientId string
tenantId string
)
ClientLoop:
for cId := range clientIds {
clientId = cId
for tId := range tenantIds {
tenantId = tId
// Skip known invalid tenants.
invalidClients := invalidClientsForTenant[tenantId]
if invalidClients == nil {
invalidClients = map[string]struct{}{}
invalidClientsForTenant[tenantId] = invalidClients
}
if _, ok := invalidClients[clientId]; ok {
continue
}
if verify {
if !azure_entra.TenantExists(logCtx, client, tenantId) {
// Tenant doesn't exist
delete(tenantIds, tenantId)
continue
}
// Tenant exists, ensure this isn't attempted as a clientId.
delete(clientIds, tenantId)
isVerified, extraData, verificationErr := serviceprincipal.VerifyCredentials(ctx, client, tenantId, clientId, clientSecret)
// Handle errors.
if verificationErr != nil {
switch {
case errors.Is(verificationErr, serviceprincipal.ErrConditionalAccessPolicy):
// Do nothing.
case errors.Is(verificationErr, serviceprincipal.ErrSecretInvalid):
continue ClientLoop
case errors.Is(verificationErr, serviceprincipal.ErrSecretExpired):
continue SecretLoop
case errors.Is(verificationErr, serviceprincipal.ErrTenantNotFound):
// Tenant doesn't exist. This shouldn't happen with the check above.
delete(tenantIds, tenantId)
continue
case errors.Is(verificationErr, serviceprincipal.ErrClientNotFoundInTenant):
// Tenant is valid but the ClientID doesn't exist.
invalidClients[clientId] = struct{}{}
continue
}
}
// The result is verified or there's only one associated client and tenant.
if isVerified || (len(clientIds) == 1 && len(tenantIds) == 1) {
r = createResult(tenantId, clientId, clientSecret, isVerified, extraData, verificationErr)
break ClientLoop
}
}
}
}
if r == nil {
// Only include the clientId and tenantId if we're confident which one it is.
if len(clientIds) != 1 {
clientId = ""
}
if len(tenantIds) != 1 {
tenantId = ""
}
r = createResult(tenantId, clientId, clientSecret, false, nil, nil)
}
results = append(results, *r)
}
return results
}
func createResult(tenantId string, clientId string, clientSecret string, verified bool, extraData map[string]string, err error) *detectors.Result {
r := &detectors.Result{
DetectorType: detectorspb.DetectorType_Azure,
Raw: []byte(clientSecret),
ExtraData: extraData,
Verified: verified,
Redacted: clientSecret[:5] + "...",
}
r.SetVerificationError(err, clientSecret)
// Tenant ID is required for verification, but it may not always be present.
// e.g., ACR or Azure SQL use client id+secret without tenant.
if clientId != "" && tenantId != "" {
var sb strings.Builder
sb.WriteString(`{`)
sb.WriteString(`"clientSecret":"` + clientSecret + `"`)
sb.WriteString(`,"clientId":"` + clientId + `"`)
sb.WriteString(`,"tenantId":"` + tenantId + `"`)
sb.WriteString(`}`)
r.RawV2 = []byte(sb.String())
}
return r
}
func findSecretMatches(data string) map[string]struct{} {
uniqueMatches := make(map[string]struct{})
for _, match := range SecretPat.FindAllStringSubmatch(data, -1) {
uniqueMatches[match[1]] = struct{}{}
}
return uniqueMatches
}
================================================
FILE: pkg/detectors/azure_entra/serviceprincipal/v2/spv2_integration_test.go
================================================
//go:build detectors
// +build detectors
package v2
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAzure_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("AZURE_SECRET")
secretInactive := testSecrets.MustGetField("AZURE_INACTIVE")
id := testSecrets.MustGetField("AZURE_ID")
tenantId := testSecrets.MustGetField("AZURE_TENANT_ID")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf(`
tenant_id=%s
client_id=%s
client_secret=%s
client_secret=%s
`, tenantId, id, secretInactive, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Azure,
Redacted: id,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf(`
tenant_id=%s
client_id=%s
client_secret=%s
`, tenantId, id, secretInactive)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Azure,
Redacted: id,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Azure.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
for i := range got {
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Azure.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azure_entra/serviceprincipal/v2/spv2_test.go
================================================
package v2
import (
"testing"
"github.com/google/go-cmp/cmp"
)
type testCase struct {
Input string
Expected map[string]struct{}
}
func Test_FindClientSecretMatches(t *testing.T) {
cases := map[string]testCase{
"secret": {
Input: `servicePrincipal:
tenantId: "608e4ac4-2ca8-40dd-a046-4064540a1cde"
clientId: "1474bfe8-663c-486e-9daf-f1f580302218"
clientSecret: "R028Q~ZOKzgCYyhr1ZJNNKhP8gUcD3Dpy2jMqaXf"
agentImage: "karbar.azurecr.io/kar-agent"`,
Expected: map[string]struct{}{
"R028Q~ZOKzgCYyhr1ZJNNKhP8gUcD3Dpy2jMqaXf": {},
},
},
"secret_start_with_dash": {
Input: `azure:
active-directory:
enabled: true
profile:
tenant-id: 11111111-1111-1111-1111-111111111111
credential:
client-id: 00000000-0000-0000-0000-000000000000
client-secret: -bs8Q~F9mPSWiDihY0NIpcQjAWoUoQ.c-seM-c0_`,
Expected: map[string]struct{}{
"-bs8Q~F9mPSWiDihY0NIpcQjAWoUoQ.c-seM-c0_": {},
},
},
"secret_end_with_dash": {
Input: `OPENID_CLIENT_ID=8595f61a-109a-497d-8c8f-566b733e95fe
OPENID_CLIENT_SECRET=aZ78Q~C~--E4dgsHZklBWtAw0mdajUHAaXXG5cq-
OPENID_GRANT_TYPE=client_credentials`,
Expected: map[string]struct{}{
"aZ78Q~C~--E4dgsHZklBWtAw0mdajUHAaXXG5cq-": {},
},
},
"client_secret": {
Input: ` "RequestBody": "client_id=4cb7565b-9ff0-49ed-b317-4dace4a70396\u0026grant_type=client_credentials\u0026client_info=1\u0026client_secret=-6s8Q~.Q9CKMOXHGs_BA3ig2wUzyDRyulhWEOc3u\u0026claims=%7B%22access_token%22%3A\u002B%7B%22xms_cc%22%3A\u002B%7B%22values%22%3A\u002B%5B%22CP1%22%5D%7D%7D%7D\u0026scope=https%3A%2F%2Fmanagement.azure.com%2F.default",`,
Expected: map[string]struct{}{
"-6s8Q~.Q9CKMOXHGs_BA3ig2wUzyDRyulhWEOc3u": {},
},
},
}
for name, test := range cases {
t.Run(name, func(t *testing.T) {
matches := findSecretMatches(test.Input)
if len(matches) == 0 {
if len(test.Expected) != 0 {
t.Fatalf("no matches found, expected: %v", test.Expected)
return
} else {
return
}
}
if diff := cmp.Diff(test.Expected, matches); diff != "" {
t.Errorf("%s diff: (-want +got)\n%s", name, diff)
}
})
}
}
================================================
FILE: pkg/detectors/azure_openai/azure_openai.go
================================================
package azure_openai
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
)
// Scanner detects API keys for Azure's OpenAI service.
// https://learn.microsoft.com/en-us/azure/ai-services/openai/reference
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
// TODO: Investigate custom `azure-api.net` endpoints.
// https://github.com/openai/openai-python#microsoft-azure-openai
azureUrlPat = regexp.MustCompile(`(?i)([a-z0-9-]+\.openai\.azure\.com)`)
azureKeyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"api[_.-]?key", "openai[_.-]?key"}) + `\b(?-i:([a-f0-9]{32}))\b`)
invalidServices = simple.NewCache[struct{}]()
)
// 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{".openai.azure.com"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureOpenAI
}
func (s Scanner) Description() string {
return "Azure OpenAI provides various AI models and services. The API keys can be used to access and interact with these models and services."
}
// FromData will find and optionally verify OpenAI 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)
// De-duplicate results.
tokens := make(map[string]struct{})
for _, match := range azureKeyPat.FindAllStringSubmatch(dataStr, -1) {
tokens[match[1]] = struct{}{}
}
if len(tokens) == 0 {
return
}
urls := make(map[string]struct{})
for _, match := range azureUrlPat.FindAllStringSubmatch(dataStr, -1) {
u := match[1]
if invalidServices.Exists(u) {
continue
}
urls[u] = struct{}{}
}
// Process results.
logCtx := logContext.AddLogger(ctx)
for token := range tokens {
s1 := detectors.Result{
DetectorType: s.Type(),
Redacted: token[:3] + "..." + token[25:],
Raw: []byte(token),
}
for url := range urls {
if verify {
client := s.client
if client == nil {
client = common.SaneHttpClient()
}
isVerified, extraData, verificationErr := verifyAzureToken(logCtx, client, url, token)
if isVerified || len(urls) == 1 {
s1.RawV2 = []byte(token + ":" + url)
s1.Verified = isVerified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr, token)
break
}
// Instance doesn't exist.
// Verification issue: lookup azsdk-east-us.openai.azure.com: no such host
if verificationErr != nil && strings.Contains(verificationErr.Error(), "no such host") {
delete(urls, url)
invalidServices.Set(url, struct{}{})
}
}
}
results = append(results, s1)
}
return
}
func verifyAzureToken(ctx logContext.Context, client *http.Client, baseUrl, token string) (bool, map[string]string, error) {
// TODO: Replace this with a more suitable long-term endpoint.
// Most endpoints require additional info, e.g., deployment name, which complicates verification.
// This may be retired in the future, so we should look for another candidate.
// https://learn.microsoft.com/en-us/answers/questions/1371786/get-azure-openai-deployments-in-api
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s/openai/deployments?api-version=2023-03-15-preview", baseUrl), nil)
if err != nil {
return false, nil, nil
}
req.Header.Set("Api-Key", token)
req.Header.Set("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
body, err := io.ReadAll(res.Body)
if err != nil {
return false, nil, err
}
var deployments deploymentsResponse
if err := json.Unmarshal(body, &deployments); err != nil {
if json.Valid(body) {
return false, nil, fmt.Errorf("failed to decode response %s: %w", req.URL, err)
} else {
// If the response isn't JSON it's highly unlikely to be valid.
return false, nil, nil
}
}
// JSON unmarshal doesn't check whether the structure actually matches.
if deployments.Object == "" {
return false, nil, nil
}
// No extra data available at the moment.
return true, nil, nil
case http.StatusUnauthorized:
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected response status %d for %s", res.StatusCode, req.URL)
}
}
type deploymentsResponse struct {
Data []deployment `json:"data"`
Object string `json:"object"`
}
type deployment struct {
ID string `json:"id"`
}
================================================
FILE: pkg/detectors/azure_openai/azure_openai_integration_test.go
================================================
//go:build detectors
// +build detectors
package azure_openai
import (
"context"
"fmt"
"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/detectorspb"
"testing"
"time"
)
func TestAzureOpenAI_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("AZUREOPENAI")
inactiveSecret := testSecrets.MustGetField("AZUREOPENAI_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azureopenai secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureOpenAI,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azureopenai secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureOpenAI,
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 azureopenai secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureOpenAI,
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 azureopenai secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureOpenAI,
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("Azureopenai.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Azureopenai.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azure_openai/azure_openai_test.go
================================================
package azure_openai
import (
"context"
"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"
)
func TestAzureOpenAI_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "Generic environment variables",
input: `export OPENAI_API_VERSION=2023-07-15-preview
export OPENAI_API_TYPE=AZURE
export OPENAI_API_BASE=https://james-test-gpt4.openai.azure.com/
export OPENAI_API_KEY=3397348fcdcb4a5fbeb6cceb5a6a284f`,
want: []string{"3397348fcdcb4a5fbeb6cceb5a6a284f"},
},
{
name: "Generic non-structured",
input: `# {'input': ['This is a test query.'], 'engine': 'text-embedding-ada-002'}
# url /openai/deployments/text-embedding-ada-002/embeddings?api-version=2022-12-01
# params {'input': ['This is a test query.'], 'encoding_format': 'base64'}
# headers None
# message='Request to OpenAI API' method=post path=https://notebook-openai01.openai.azure.com/openai/deployments/text-embedding-ada-002/embeddings?api-version=2022-12-01
# api_version=2022-12-01 data='{"input": ["This is a test query."], "encoding_format": "base64"}' message='Post details'
# https://notebook-openai01.openai.azure.com/openai/deployments/text-embedding-ada-002/embeddings?api-version=2022-12-01
# {'X-OpenAI-Client-User-Agent': '{"bindings_version": "0.27.6", "httplib": "requests", "lang": "python", "lang_version": "3.11.2", "platform": "macOS-13.2-arm64-arm-64bit",
"publisher": "openai", "uname": "Darwin 22.3.0 Darwin Kernel Version 22.3.0: Thu Jan 5 20:48:54 PST 2023; root:xnu-8792.81.2~2/RELEASE_ARM64_T6000 arm64 arm"}', 'User-Agent': 'OpenAI/v1 PythonBindings/0.27.6', 'api-key': '49eb7c2d3acd41f4ac31fef59ceacbba', 'OpenAI-Debug': 'true', 'Content-Type': 'application/json'}`,
want: []string{"49eb7c2d3acd41f4ac31fef59ceacbba"},
},
{
name: "Python",
input: `import openai
openai.api_key = '1bb7dff73fe449de829363ea03bab134'
openai.api_base = "https://hrcop-openai.openai.azure.com/"
`,
want: []string{"1bb7dff73fe449de829363ea03bab134"},
},
{
name: "Python environment variables",
input: `os.environ["OPENAI_API_TYPE"] = "azure"
os.environ["OPENAI_API_VERSION"] = "2023-03-15-preview"
os.environ["OPENAI_API_BASE"] = "https://superhackathonai101-openai.openai.azure.com/"
os.environ["OPENAI_API_KEY"] = '1bb7dde73fe449de229361ea03bab234'`,
want: []string{"1bb7dde73fe449de229361ea03bab234"},
},
{
name: "TypeScript",
input: `import OpenAI from "openai";
export const openai = new OpenAI({
apiKey: "3375e3ad9a874cd6bd954b6f163be84f",
baseURL:
"https://kumar-azure.openai.azure.com/openai/deployments/ChatAutoUpdate",
defaultQuery: { "api-version": "2023-06-01-preview" },
});`,
want: []string{"3375e3ad9a874cd6bd954b6f163be84f"},
},
{
name: "OpenAi key name",
input: `{
"IsEncrypted": false,
"Values": {
"AZURE_OPENAI_ENDPOINT": "https://bcdemo-openai.openai.azure.com/",
"AZURE_OPENAI_KEY": "57d2de35873840b5ad59d742e90e974e"
}
}`,
want: []string{"57d2de35873840b5ad59d742e90e974e"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/azure_storage/storage.go
================================================
package azure_storage
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/xml"
"fmt"
"io"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses
namePat = regexp.MustCompile(`(?i:Account[_.-]?Name|Storage[_.-]?(?:Account|Name))(?:.|\s){0,20}?\b([a-z0-9]{3,24})\b|([a-z0-9]{3,24})(?i:\.blob\.core\.windows\.net)`) // Names can only be lowercase alphanumeric.
keyPat = regexp.MustCompile(`(?i:(?:Access|Account|Storage)[_.-]?Key)(?:.|\s){0,25}?([a-zA-Z0-9+\/-]{86,88}={0,2})`)
// https://learn.microsoft.com/en-us/azure/storage/common/storage-use-emulator
testNames = map[string]struct{}{
"devstoreaccount1": {},
"storagesample": {},
}
testKeys = map[string]struct{}{
"Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==": {},
}
)
func (s Scanner) Keywords() []string {
return []string{
"DefaultEndpointsProtocol=http", "EndpointSuffix", "core.windows.net",
"AccountName", "Account_Name", "Account.Name", "Account-Name",
"StorageAccount", "Storage_Account", "Storage.Account", "Storage-Account",
"AccountKey", "Account_Key", "Account.Key", "Account-Key",
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureStorage
}
func (s Scanner) Description() string {
return "Azure Storage is a Microsoft-managed cloud service that provides storage that is highly available, secure, durable, scalable, and redundant. Azure Storage Account keys can be used to access and manage data within storage accounts."
}
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
dataStr := string(data)
// Deduplicate results.
names := make(map[string]struct{})
for _, matches := range namePat.FindAllStringSubmatch(dataStr, -1) {
var name string
if matches[1] != "" {
name = matches[1]
} else {
name = matches[2]
}
if _, ok := testNames[name]; ok {
continue
}
names[name] = struct{}{}
}
if len(names) == 0 {
return results, nil
}
keys := make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
key := matches[1]
if _, ok := testKeys[key]; ok {
continue
}
keys[key] = struct{}{}
}
if len(keys) == 0 {
return results, nil
}
// Check results.
for name := range names {
var s1 detectors.Result
for key := range keys {
s1 = detectors.Result{
DetectorType: s.Type(),
Raw: []byte(key),
RawV2: []byte(fmt.Sprintf(`{"accountName":"%s","accountKey":"%s"}`, name, key)),
ExtraData: map[string]string{
"Account_name": name,
},
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := s.verifyMatch(ctx, client, name, key, s1.ExtraData)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, key)
}
results = append(results, s1)
if s1.Verified {
break
}
}
}
return results, nil
}
type storageResponse struct {
Containers struct {
Container []container `xml:"Container"`
} `xml:"Containers"`
}
type container struct {
Name string `xml:"Name"`
}
func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, name string, key string, extraData map[string]string) (bool, error) {
// https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key
now := time.Now().UTC().Format(http.TimeFormat)
stringToSign := "GET\n\n\n\n\n\n\n\n\n\n\n\nx-ms-date:" + now + "\nx-ms-version:2019-12-12\n/" + name + "/\ncomp:list"
accountKeyBytes, _ := base64.StdEncoding.DecodeString(key)
h := hmac.New(sha256.New, accountKeyBytes)
h.Write([]byte(stringToSign))
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
url := "https://" + name + ".blob.core.windows.net/?comp=list"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return false, err
}
req.Header.Set("x-ms-date", now)
req.Header.Set("x-ms-version", "2019-12-12")
req.Header.Set("Authorization", "SharedKey "+name+":"+signature)
res, err := client.Do(req)
if err != nil {
// If the host is not found, we can assume that the accountName is not valid
if strings.Contains(err.Error(), "no such host") {
return false, nil
}
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
// parse response
response := storageResponse{}
if err := xml.NewDecoder(res.Body).Decode(&response); err != nil {
return false, err
}
// update the extra data with container names only
if len(response.Containers.Container) > 0 {
var b strings.Builder
for i, c := range response.Containers.Container {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(c.Name)
}
extraData["container_names"] = b.String()
}
return true, nil
case http.StatusForbidden:
// 403 if account id or key is invalid, or if the account is disabled
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
================================================
FILE: pkg/detectors/azure_storage/storage_integration_test.go
================================================
//go:build detectors
// +build detectors
package azure_storage
import (
"context"
"fmt"
"strings"
"testing"
"time"
regexp "github.com/wasilibs/go-re2"
"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/detectorspb"
)
func TestAzurestorage_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("AZURE_STORAGE")
inactiveSecret := testSecrets.MustGetField("AZURE_STORAGE_INACTIVE")
accountNamePat := regexp.MustCompile(`AccountName=(?P[^;]+);AccountKey`)
accountName := accountNamePat.FindStringSubmatch(secret)[1]
validKeyInvalidAccountName := strings.Replace(secret, accountName, "invalid", 1)
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azurestorage secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureStorage,
Verified: true,
ExtraData: map[string]string{
"account_name": "teststoragebytruffle",
},
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azurestorage secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureStorage,
Verified: false,
ExtraData: map[string]string{
"account_name": "teststoragebytruffle",
},
},
},
wantErr: 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,
},
{
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 azurestorage secret %s within", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_AzureStorage,
Verified: false,
ExtraData: map[string]string{
"account_name": "teststoragebytruffle",
},
}
r.SetVerificationError(fmt.Errorf("context deadline exceeded"), secret)
return []detectors.Result{r}
}(),
wantErr: false,
},
{
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 azurestorage secret %s within", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_AzureStorage,
Verified: false,
ExtraData: map[string]string{
"account_name": "teststoragebytruffle",
},
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 404"), secret)
return []detectors.Result{r}
}(),
wantErr: false,
},
{
name: "found secret with invalid account name",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azurestorage secret %s within", validKeyInvalidAccountName)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureStorage,
Verified: false,
ExtraData: map[string]string{
"account_name": "invalid",
},
},
},
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("Azuretorage.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", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Azurestorage.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azure_storage/storage_test.go
================================================
package azure_storage
import (
"context"
"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"
)
func TestAzureStorage_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
// True Positive
// CONNECTION STRINGS
{
name: `connection_string_1`,
input: `DefaultEndpointsProtocol=https;AccountName=storagetest123;AccountKey=YutGV0Vlauqsobd5tPWz2AKwHhBXMEWsAH+rSbz0UZUfaMVj1CFrcNQK47ygmrC4vHmc7eOp1LdM+AStk5mMyA==;EndpointSuffix=core.windows.net`,
want: []string{`{"accountName":"storagetest123","accountKey":"YutGV0Vlauqsobd5tPWz2AKwHhBXMEWsAH+rSbz0UZUfaMVj1CFrcNQK47ygmrC4vHmc7eOp1LdM+AStk5mMyA=="}`},
},
{
name: `connection_string_2`,
input: `EndpointSuffix=core.windows.net;AccountKey=ldlKgoKPJhRjPJTkaC5c/QNqtu4sVQRc/teGJ0MZHbDYEHdvBV5z8JEfJK+evE87D7U8TzMZ0G2C+ASt2B4ifg==;AccountName=storagetest123;DefaultEndpointsProtocol=http`,
want: []string{`{"accountName":"storagetest123","accountKey":"ldlKgoKPJhRjPJTkaC5c/QNqtu4sVQRc/teGJ0MZHbDYEHdvBV5z8JEfJK+evE87D7U8TzMZ0G2C+ASt2B4ifg=="}`},
},
{
name: `connection_string_3`,
input: ` public const string SharedStorageKey = "DefaultEndpointsProtocol=https;AccountName=huntappstorage;AccountKey=rrttFty/b52ET/e8VqpMSN+ZqAUP7hcXVkdekrPX58gsMZyOCrE+igN07t3lyi7tAV0+OrJFBaDtMe06YJ2tFw==;EndpointSuffix=core.windows.net";`,
want: []string{`{"accountName":"huntappstorage","accountKey":"rrttFty/b52ET/e8VqpMSN+ZqAUP7hcXVkdekrPX58gsMZyOCrE+igN07t3lyi7tAV0+OrJFBaDtMe06YJ2tFw=="}`},
},
{
name: `connection_string_multiline`,
input: `
export const DevelopmentConnectionString = 'DefaultEndpointsProtocol=http;AccountName=macdemostorage;
AccountKey=Jby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;
QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;';`,
want: []string{`{"accountName":"macdemostorage","accountKey":"Jby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="}`},
},
// LANGUAGES
// TODO:
// - https://github.com/Satyamk21/az204/blob/75f340c5bbfb34c1477a6885e216d5ae0972a380/Lab%203.txt#L22
// https://github.com/facebookincubator/velox/blob/98e958c0df498efd7cf44a2078cc71caeb7aed23/velox/connectors/hive/storage_adapters/abfs/tests/AzuriteServer.h#L32-L36
{
name: `cpp`,
input: `static const std::string AzuriteAccountName{"storagetest123"};
static const std::string AzuriteContainerName{"test"};
// the default key of Azurite Server used for connection
static const std::string AzuriteAccountKey{
"qYaZm3m8+Z2aYAiSDzzvStkTUgZXl29U76lDJ0qiob7bbV4g7kjtwO+FI2QoptGdZgEdtsAYzG8T0hl5TeftWA=="};`,
want: []string{`{"accountName":"storagetest123","accountKey":"qYaZm3m8+Z2aYAiSDzzvStkTUgZXl29U76lDJ0qiob7bbV4g7kjtwO+FI2QoptGdZgEdtsAYzG8T0hl5TeftWA=="}`},
},
// https://github.com/MicrosoftDX/Dash/blob/03c4bb55f9e84fd03ee943559c128c4d5c2a31c2/DashServer.Tests/RequestAuthTests.cs#L29
{
name: `dotnet1`,
input: ` _ctx = InitializeConfigAndCreateTestBlobs(ctx, "datax1", new Dictionary
{
{ "AccountName", "dashstorage1" },
{ "AccountKey", "8jqRVtXUWiEthgIhR+dFwrB8gh3lFuquvJQ1v4eabObIj7okI1cZIuzY8zZHmEdpcC0f+XlUkbFwAhjTfyrLIg==" },
{ "SecondaryAccountKey", "Klari9ZbVdFQ35aULCfqqehCsd136amhusMHWynTpz2Pg+GyQMJw3GH177hvEQbaZ2oeRYk3jw0mIaV3ehNIRg==" },
},`,
want: []string{
`{"accountName":"dashstorage1","accountKey":"8jqRVtXUWiEthgIhR+dFwrB8gh3lFuquvJQ1v4eabObIj7okI1cZIuzY8zZHmEdpcC0f+XlUkbFwAhjTfyrLIg=="}`,
`{"accountName":"dashstorage1","accountKey":"Klari9ZbVdFQ35aULCfqqehCsd136amhusMHWynTpz2Pg+GyQMJw3GH177hvEQbaZ2oeRYk3jw0mIaV3ehNIRg=="}`,
},
},
// https://github.com/Satyamk21/az204/blob/75f340c5bbfb34c1477a6885e216d5ae0972a380/Lab%203.txt#L11
{
name: `dotnet2`,
input: `public class Program
{
private const string blobServiceEndpoint = "https://k21storagemedia.blob.core.windows.net/";
private const string storageAccountName = "k21storagemedia";
private const string storageAccountKey = "DFdukxfl0SwO4NB91bi/FTPh9BMEKr6Z5wWf+tGDfXMakXvGVp/NDzAUjWc/9171OqoDvXSj1o8N+AStUk1GXg==";
//The following code to create a new asynchronous Main method
public static async Task Main(string[] args)`,
want: []string{`{"accountName":"k21storagemedia","accountKey":"DFdukxfl0SwO4NB91bi/FTPh9BMEKr6Z5wWf+tGDfXMakXvGVp/NDzAUjWc/9171OqoDvXSj1o8N+AStUk1GXg=="}`},
},
// https://github.com/apache/camel/blob/main/test-infra/camel-test-infra-azure-common/src/test/java/org/apache/camel/test/infra/azure/common/services/AzuriteContainer.java#L25-L27
{
name: `java`,
input: `public class AzuriteContainer extends GenericContainer {
public static final String DEFAULT_ACCOUNT_NAME = "storagetest123";
public static final String DEFAULT_ACCOUNT_KEY
= "qYaZm3m8+Z2aYAiSDzzvStkTUgZXl29U76lDJ0qiob7bbV4g7kjtwO+FI2QoptGdZgEdtsAYzG8T0hl5TeftWA==";
public static final String IMAGE_NAME = "mcr.microsoft.com/azure-storage/azurite:3.27.0";`,
want: []string{`{"accountName":"storagetest123","accountKey":"qYaZm3m8+Z2aYAiSDzzvStkTUgZXl29U76lDJ0qiob7bbV4g7kjtwO+FI2QoptGdZgEdtsAYzG8T0hl5TeftWA=="}`},
},
// https://github.com/Azure/azure-storage-node/blob/6873387fc65bad6d577babe278be2ee2e6071493/test/common/connectionstringparsertests.js
{
name: `javascript`,
input: ` var parsedConnectionString = ServiceSettings.parseAndValidateKeys(defaultConnectionString + endpointsConnectionString, validKeys);
assert.equal(parsedConnectionString['DefaultEndpointsProtocol'], 'https');
assert.equal(parsedConnectionString['AccountName'], 'storagetest123');
assert.equal(parsedConnectionString['AccountKey'], 'KWPLd0rpW2T0U7K2pVpF8rYr1BgYtR7wYQk33AYiXeUoquiaY6o0TWqduxmPHlqeCNZ3LU0DHptbeIHy5l/Yhg==');
assert.equal(parsedConnectionString['BlobEndpoint'], 'myBlobEndpoint');
assert.equal(parsedConnectionString['QueueEndpoint'], 'myQueueEndpoint');
assert.equal(parsedConnectionString['TableEndpoint'], 'myTableEndpoint');`,
want: []string{`{"accountName":"storagetest123","accountKey":"KWPLd0rpW2T0U7K2pVpF8rYr1BgYtR7wYQk33AYiXeUoquiaY6o0TWqduxmPHlqeCNZ3LU0DHptbeIHy5l/Yhg=="}`},
},
// https://github.com/nextcloud/server/blob/81a9e19ace190ea0a64d52d95d341e25c7ad618b/tests/preseed-config.php#L89
{
name: `php`,
input: ` 'arguments' => [
'container' => 'test',
'account_name' => 'storagetest123',
'account_key' => 'qYaZm3m8+Z2aYAiSDzzvStkTUgZXl29U76lDJ0qiob7bbV4g7kjtwO+FI2QoptGdZgEdtsAYzG8T0hl5TeftWA==',
'endpoint' => 'http://' . (getenv('DRONE') === 'true' ? 'azurite' : 'localhost') . ':10000/devstoreaccount1',
'autocreate' => true
]`,
want: []string{`{"accountName":"storagetest123","accountKey":"qYaZm3m8+Z2aYAiSDzzvStkTUgZXl29U76lDJ0qiob7bbV4g7kjtwO+FI2QoptGdZgEdtsAYzG8T0hl5TeftWA=="}`},
},
// https://github.com/Azure/azure-sdk-for-js/blob/2719dcfbe835a2da3003876dcb5d77efba95f912/sdk/cosmosdb/cosmos/test/public/common/_fakeTestSecrets.ts
{
name: `typescript`,
input: `export const name =
process.env.ACCOUNT_NAME || "storagename123";
export const key =
process.env.ACCOUNT_KEY ||
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";`,
want: []string{`{"accountName":"storagename123","accountKey":"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="}`},
},
// FORMATS
// TODO: Doesn't work.
// https://github.com/Azure/azure-quickstart-templates/blob/03e792429fbc65c9353335611933746364590b22/quickstarts/microsoft.datafactory/data-factory-hive-transformation/azuredeploy.parameters.json#L9C38-L9C38
// {
// name: `json`,
// input: ` "storageAccountName": {
// "value": "changemeazurestorage"
//},
//"storageAccountKey": {
// "value": "YA1gKAMY34PeVgEWPF8FdbQO+U0nFkd3SaFE4d32K16AYL/DowrTYun8anOdAiCnMkCiRYm+PxUh5mw7a7lVcA=="
//},`,
// want: []string{`{"accountName":"storagetest123","accountKey":"qYaZm3m8+Z2aYAiSDzzvStkTUgZXl29U76lDJ0qiob7bbV4g7kjtwO+FI2QoptGdZgEdtsAYzG8T0hl5TeftWA=="}`},
// },
// https://github.com/ClickHouse/ClickHouse/blob/eba52b318d67d85330c9c1781499b7ff27fb7c0e/tests/integration/test_storage_azure_blob_storage/configs/named_collections.xml
{
name: `xml`,
input: ` storagetest123qYaZm3m8+Z2aYAiSDzzvStkTUgZXl29U76lDJ0qiob7bbV4g7kjtwO+FI2QoptGdZgEdtsAYzG8T0hl5TeftWA==`,
want: []string{`{"accountName":"storagetest123","accountKey":"qYaZm3m8+Z2aYAiSDzzvStkTUgZXl29U76lDJ0qiob7bbV4g7kjtwO+FI2QoptGdZgEdtsAYzG8T0hl5TeftWA=="}`},
},
// https://github.com/hubblestack/hubble/blob/f9b7bf38752bd16b27d050a3b8787652a1c6319b/hubblestack/fileserver/azurefs.py
{
name: `yaml1`,
input: ` azurefs:
- account_name: mystorage
account_key: 'fNH9cRp0+qVIVYZ+5rnZAhHc9ycOUcJnHtzpfOr0W0sxrtL2KVLuMe1xDfLwmfed+JJInZaEdWVCPHD4d/oqeA=='
container_name: my_container
proxy: 10.10.10.10:8080`,
want: []string{`{"accountName":"mystorage","accountKey":"fNH9cRp0+qVIVYZ+5rnZAhHc9ycOUcJnHtzpfOr0W0sxrtL2KVLuMe1xDfLwmfed+JJInZaEdWVCPHD4d/oqeA=="}`},
},
{
name: `yaml2`,
input: ` - name: filesharevolume
azureFile:
sharename: containershare
storageAccountName: newstore100033323
storageAccountKey: Ar4/2iY8L0rEMeQaijINnfaMJr7vqjfbPgmJayw6Pu5l9ZI+GrFDm1uIWOqXk5RQLrTiXfBwWY6hAbPEIQqy1g==`,
want: []string{`{"accountName":"newstore100033323","accountKey":"Ar4/2iY8L0rEMeQaijINnfaMJr7vqjfbPgmJayw6Pu5l9ZI+GrFDm1uIWOqXk5RQLrTiXfBwWY6hAbPEIQqy1g=="}`},
},
// This was manually base64-decoded since that doesn't work in unit tests.
// https://github.com/fabric8io/configmapcontroller/blob/master/vendor/k8s.io/kubernetes/examples/azure_file/secret/azure-secret.yaml
{
name: `yaml_3`,
input: `apiVersion: v1
kind: Secret
metadata:
name: azure-secret
type: Opaque
data:
azurestorageaccountname: k8stest
azurestorageaccountkey: xIF1zJbnnojFLMSkBp50mx02rHsMK2sjU7mFt4L13hoB7drAaJ8jD6+A443jJogV7y2FUOhQCWPmM6YaNHy7qg==
`,
want: []string{`{"accountName":"k8stest","accountKey":"xIF1zJbnnojFLMSkBp50mx02rHsMK2sjU7mFt4L13hoB7drAaJ8jD6+A443jJogV7y2FUOhQCWPmM6YaNHy7qg=="}`},
},
// MISC
// https://github.com/Azure-Samples/nested-virtualization-image-builder/blob/cf0373a421343b00ce3d261be99ddced80deb55b/README.md?plain=1#L54
{
name: `blob_url`,
input: `"name": "storagetest123.blob.core.windows.net", "accountKey":"hGeB3WqDyx0mGsQMsQDl+gmnXa51ZODiBtcXJpMoRhPjkDm79f9ErNfaYizXm7nkElix8n2uBwNk6KY8Rc866w=="`,
want: []string{`{"accountName":"storagetest123","accountKey":"hGeB3WqDyx0mGsQMsQDl+gmnXa51ZODiBtcXJpMoRhPjkDm79f9ErNfaYizXm7nkElix8n2uBwNk6KY8Rc866w=="}`},
},
{
name: `random_cli_1`,
input: `go run .\main.go -debug -dest="https://kenfau.blob.core.windows.net/ss3/" -AzureDefaultAccountName="kenfoo" -AzureDefaultAccountKey="hGeB3WqDyx0mGsQMsQDl+gmnXa51ZODiBtcXJpMoRhPjkDm79f9ErNfaYizXm7nkElix8n2uBwNk6KY8Rc866w=="`,
want: []string{
`{"accountName":"kenfau","accountKey":"hGeB3WqDyx0mGsQMsQDl+gmnXa51ZODiBtcXJpMoRhPjkDm79f9ErNfaYizXm7nkElix8n2uBwNk6KY8Rc866w=="}`,
`{"accountName":"kenfoo","accountKey":"hGeB3WqDyx0mGsQMsQDl+gmnXa51ZODiBtcXJpMoRhPjkDm79f9ErNfaYizXm7nkElix8n2uBwNk6KY8Rc866w=="}`,
},
},
// - https://github.com/nwoolls/AzureStorageCleanup/blob/980e5cb163c78e9446e70d2513ba5a7ed9051a7a/README.md?plain=1#L24
{
name: `random_cli_2`,
input: `AzureStorageCleanup.exe
-storagename storageaccount
-storagekey hGeB3WqDyx0mGsQMsQDl+gmnXa51ZODiBtcXJpMoRhPjkDm79f9ErNfaYizXm7nkElix8n2uBwNk6KY8Rc866w==
-container sqlbackup
-mindaysold 60
-searchpattern .*
-recursive
-whatif`,
want: []string{`{"accountName":"storageaccount","accountKey":"hGeB3WqDyx0mGsQMsQDl+gmnXa51ZODiBtcXJpMoRhPjkDm79f9ErNfaYizXm7nkElix8n2uBwNk6KY8Rc866w=="}`},
},
// https://github.com/dahlej/rpi-spark-titantic/blob/d00b8f5b4696aeb2113e9452c24bb31b7f9a0242/tmp.txt#L9
{
name: `random_cli_3`,
input: `$ bin/spark-submit --master \
k8s://test-cluster.eastus2.cloudapp.azure.com:443 \
--deploy-mode cluster \
--name copyLocations \
--class io.timpark.CopyData \
--conf spark.copydata.containerpath=wasb://containers@storagetest123.blob.core.windows.net \
--conf spark.copydata.storageaccount=storagetest123 \
--conf spark.copydata.storageaccountkey=hGeB3WqDyx0mGsQMsQDl+gmnXa51ZODiBtcXJpMoRhPjkDm79f9ErNfaYizXm7nkElix8n2uBwNk6KY8Rc866w== \`,
want: []string{`{"accountName":"storagetest123","accountKey":"hGeB3WqDyx0mGsQMsQDl+gmnXa51ZODiBtcXJpMoRhPjkDm79f9ErNfaYizXm7nkElix8n2uBwNk6KY8Rc866w=="}`},
},
{
name: `custom_config_1`,
input: `driver := ArtifactDriver{
AccountName: "storagetest123",
AccountKey: "qYaZm3m8+Z2aYAiSDzzvStkTUgZXl29U76lDJ0qiob7bbV4g7kjtwO+FI2QoptGdZgEdtsAYzG8T0hl5TeftWA==",
Container: "test",
}`,
want: []string{`{"accountName":"storagetest123","accountKey":"qYaZm3m8+Z2aYAiSDzzvStkTUgZXl29U76lDJ0qiob7bbV4g7kjtwO+FI2QoptGdZgEdtsAYzG8T0hl5TeftWA=="}`},
},
// https://github.com/MicrosoftDX/Dash/blob/master/LoadTestDotNet/GetBlobCoded.cs
{
name: `storage_account_1`,
input: ` this.Context.Add("StorageEndPoint", "http://dashstorage3.blob.core.windows.net");
this.Context.Add("StorageAccount", "dashstorage3");
this.Context.Add("AccountKey", "TP+G/9FTZRP1he1EpKilMercxSbMyqtaI9xTbc/3HqT2/FkxyIk1wVlBdemDFuYKStmlkFqHc7049l8McTd8NQ==");
this.Context.Add("SendChunked", false);`,
want: []string{`{"accountName":"dashstorage3","accountKey":"TP+G/9FTZRP1he1EpKilMercxSbMyqtaI9xTbc/3HqT2/FkxyIk1wVlBdemDFuYKStmlkFqHc7049l8McTd8NQ=="}`},
},
// https://github.com/kubecost/poc-common-configurations/blob/d626a48824a104e3089fc66ef57029f1e2212f6a/keys.txt#L18
{
name: `storage_account_2`,
input: `AZ_cloud_integration_subscriptionId:0bd50fdf-c923-4e1e-850c-196ddSAMPLE
AZ_cloud_integration_azureStorageAccount:kubecostexport
AZ_cloud_integration_azureStorageAccessKey:TP+G/9FTZRP1he1EpKilMercxSbMyqtaI9xTbc/3HqT2/FkxyIk1wVlBdemDFuYKStmlkFqHc7049l8McTd8NQ==
AZ_cloud_integration_azureStorageContainer:costexports`,
want: []string{`{"accountName":"kubecostexport","accountKey":"TP+G/9FTZRP1he1EpKilMercxSbMyqtaI9xTbc/3HqT2/FkxyIk1wVlBdemDFuYKStmlkFqHc7049l8McTd8NQ=="}`},
},
// False positives
{
name: `test_key`,
input: ` azureblockblob: TEST_BACKEND=azureblockblob://DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;`,
},
{
name: `test_key_multiline`,
input: `
export const DevelopmentConnectionString = 'DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;
AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;
QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;';`,
},
{
name: `invalid_key_1`,
input: ` docs::examples = "DefaultEndpointsProtocol=https;AccountName=mylogstorage;AccountKey=storageaccountkeybase64encoded;EndpointSuffix=core.windows.net"`,
},
{
name: `invalid_key_2`,
input: `PS C:\> Add-AzIotHubRoutingEndpoint -ResourceGroupName "myresourcegroup" -Name "myiothub" -EndpointName S1 -EndpointType AzureStorageContainer -EndpointResourceGroup resourcegroup1 -EndpointSubscriptionId 91d12343-a3de-345d-b2ea-135792468abc -ConnectionString 'DefaultEndpointsProtocol=https;AccountName=mystorage1;AccountKey=*****;EndpointSuffix=core.windows.net' -ContainerName container -Encoding json`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/azureapimanagement/repositorykey/repositorykey.go
================================================
package repositorykey
import (
"context"
"errors"
"fmt"
"net/url"
"os/exec"
"strconv"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
urlPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure", "url"}) + `([a-z0-9][a-z0-9-]{0,48}[a-z0-9]\.scm\.azure-api\.net)`)
passwordPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure", "password"}) + `\b(git&[0-9]{12}&[a-zA-Z0-9\/+]{85}[a-zA-Z0-9]==)`)
invalidHosts = simple.NewCache[struct{}]()
noSuchHostErr = errors.New("Could not resolve host")
)
const (
azureGitUsername = "apim"
)
// 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{"azure", ".scm.azure-api.net"}
}
// FromData will find and optionally verify AzureDevopsPersonalAccessToken secrets in a given set of bytes.
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
logger := logContext.AddLogger(ctx).Logger().WithName("azurecr")
dataStr := string(data)
// Deduplicate matches.
uniqueUrlsMatches := make(map[string]struct{})
uniquePasswordMatches := make(map[string]struct{})
for _, matches := range urlPat.FindAllStringSubmatch(dataStr, -1) {
uniqueUrlsMatches[strings.TrimSpace(matches[1])] = struct{}{}
}
for _, matches := range passwordPat.FindAllStringSubmatch(dataStr, -1) {
uniquePasswordMatches[strings.TrimSpace(matches[1])] = struct{}{}
}
EndpointLoop:
for urlMatch := range uniqueUrlsMatches {
for passwordMatch := range uniquePasswordMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AzureApiManagementRepositoryKey,
Raw: []byte(passwordMatch),
RawV2: []byte(urlMatch + passwordMatch),
}
if verify {
if invalidHosts.Exists(urlMatch) {
logger.V(3).Info("Skipping invalid registry", "url", urlMatch)
continue EndpointLoop
}
isVerified, err := verifyUrlPassword(ctx, urlMatch, azureGitUsername, passwordMatch)
s1.Verified = isVerified
if err != nil {
if errors.Is(err, noSuchHostErr) {
invalidHosts.Set(urlMatch, struct{}{})
continue EndpointLoop
}
s1.SetVerificationError(err, urlMatch)
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureApiManagementRepositoryKey
}
func (s Scanner) Description() string {
return "Azure API Management Repository Keys provide access to the API Management (APIM) configuration repository, allowing users to directly interact with and modify API definitions, policies, and settings. These keys enable programmatic access to APIM's Git-based repository, where configurations can be cloned, edited, and pushed back to apply changes. They are primarily used for managing API configurations as code, automating deployments, and synchronizing APIM settings across environments."
}
func gitCmdCheck() error {
if errors.Is(exec.Command("git").Run(), exec.ErrNotFound) {
return fmt.Errorf("'git' command not found in $PATH. Make sure git is installed and included in $PATH")
}
// Check the version is greater than or equal to 2.20.0
out, err := exec.Command("git", "--version").Output()
if err != nil {
return fmt.Errorf("failed to check git version: %w", err)
}
// Extract the version string using a regex to find the version numbers
var regex = regexp.MustCompile(`\d+\.\d+\.\d+`)
versionStr := regex.FindString(string(out))
versionParts := strings.Split(versionStr, ".")
// Parse version numbers
major, _ := strconv.Atoi(versionParts[0])
minor, _ := strconv.Atoi(versionParts[1])
// Compare with version 2.20.0<=x<3.0.0
if major == 2 && minor >= 20 {
return nil
}
return fmt.Errorf("git version is %s, but must be greater than or equal to 2.20.0, and less than 3.0.0", versionStr)
}
func verifyUrlPassword(_ context.Context, repoUrl, user, password string) (bool, error) {
if err := gitCmdCheck(); err != nil {
return false, err
}
parsedURL, err := url.Parse(repoUrl)
if err != nil {
return false, err
}
if parsedURL.User == nil {
parsedURL.User = url.UserPassword(user, password)
}
parsedURL.Scheme = "https" // Force HTTPS
fakeRef := "TRUFFLEHOG_CHECK_GIT_REMOTE_URL_REACHABILITY"
gitArgs := []string{"ls-remote", parsedURL.String(), "--quiet", fakeRef}
cmd := exec.Command("git", gitArgs...)
output, err := cmd.CombinedOutput()
if err != nil {
outputString := string(output)
if strings.Contains(outputString, "Authentication failed") {
return false, nil
} else if strings.Contains(outputString, "Could not resolve host") {
return false, noSuchHostErr
}
return false, err
}
return true, nil
}
================================================
FILE: pkg/detectors/azureapimanagement/repositorykey/repositorykey_integration_test.go
================================================
//go:build detectors
// +build detectors
package repositorykey
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/detectorspb"
)
func TestAxonaut_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)
}
url := testSecrets.MustGetField("AZURE_API_MGMT_REPOSITORY_KEY_URL")
inactiveUrl := testSecrets.MustGetField("AZURE_API_MGMT_REPOSITORY_KEY_URL_INACTIVE")
password := testSecrets.MustGetField("AZURE_API_MGMT_REPOSITORY_KEY_PASSWORD")
inactivePassword := testSecrets.MustGetField("AZURE_API_MGMT_REPOSITORY_KEY_PASSWORD_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azure repository url %s password %s", url, password)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureApiManagementRepositoryKey,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azure repository url %s password %s but unverified", url, inactivePassword)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureApiManagementRepositoryKey,
Verified: false,
},
},
wantErr: 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,
},
{
name: "found, host not resolved",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azure repository url %s password %s but unverified", inactiveUrl, inactivePassword)),
verify: true,
},
want: nil,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("AzureApiManagementRepositoryKey.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])
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "ExtraData", "AnalysisInfo")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AzureApiManagementRepositoryKey.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azureapimanagement/repositorykey/repositorykey_test.go
================================================
package repositorykey
import (
"context"
"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"
)
func TestAzureAPIManagementRepositoryKey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: `valid pattern`,
input: `
AZURE_URL=https://test.scm.azure-api.net
PASSWORD=git&202503251200&R2xlVEmqi+OW130dxWIDhfw1K6XKw/gxc5P9te3cwWBtnK2XkZq5k+VUAdnuX1Y0T/I5CRK9fJyBJr31SmFEYw==
`,
want: []string{"test.scm.azure-api.netgit&202503251200&R2xlVEmqi+OW130dxWIDhfw1K6XKw/gxc5P9te3cwWBtnK2XkZq5k+VUAdnuX1Y0T/I5CRK9fJyBJr31SmFEYw=="},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{url 726o3.scm.azure-api.net}{password AQAAABAAA git&303102631708&ZidF02ZVakrtuWcW00cgvhZ6YUiZbIsZ84bE3u01jOXdKv7VXr0t6DE9OtdJnUTaBAz843vSDvVpCjRFEYSJq3==}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"726o3.scm.azure-api.netgit&303102631708&ZidF02ZVakrtuWcW00cgvhZ6YUiZbIsZ84bE3u01jOXdKv7VXr0t6DE9OtdJnUTaBAz843vSDvVpCjRFEYSJq3=="},
},
{
name: `invalid host pattern`,
input: `
AZURE_URL=https://test.scm.azure.net
PASSWORD=git&202503251200&R2xlVEmqi+OW130dxWIDhfw1K6XKw/gxc5P9te3cwWBtnK2XkZq5k+VUAdnuX1Y0T/I5CRK9fJyBJr31SmFEYw==
`,
want: []string{},
},
{
name: `invalid password pattern without ==`,
input: `
AZURE_URL=https://test.scm.azure-api.net
PASSWORD=git&202503251200&R2xlVEmqi+OW130dxWIDhfw1K6XKw/gxc5P9te3cwWBtnK2XkZq5k+VUAdnuX1Y0T/I5CRK9fJyBJr31SmFEYw=
`,
want: []string{},
},
{
name: `invalid password pattern with wrong expiry date`,
input: `
AZURE_URL=https://test.scm.azure-api.net
PASSWORD=git&20250325&R2xlVEmqi+OW130dxWIDhfw1K6XKw/gxc5P9te3cwWBtnK2XkZq5k+VUAdnuX1Y0T/I5CRK9fJyBJr31SmFEYw==
`,
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("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/azureapimanagementsubscriptionkey/azureapimanagementsubscriptionkey.go
================================================
package azureapimanagementsubscriptionkey
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
urlPat = regexp.MustCompile(`https://([a-z0-9][a-z0-9-]{0,48}[a-z0-9])\.azure-api\.net`) // https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.APIM.Name/
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure", ".azure-api.net", "subscription", "key"}) + `([a-zA-Z-0-9]{32})`) // pattern for both Primary and secondary key
invalidHosts = simple.NewCache[struct{}]()
noSuchHostErr = errors.New("no such host")
)
// 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{".azure-api.net"}
}
// FromData will find and optionally verify Azure Subscription keys in a given set of bytes.
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
logger := logContext.AddLogger(ctx).Logger().WithName("azureapimanagementsubscriptionkey")
dataStr := string(data)
urlMatchesUnique := make(map[string]struct{})
for _, urlMatch := range urlPat.FindAllStringSubmatch(dataStr, -1) {
urlMatchesUnique[urlMatch[0]] = struct{}{}
}
keyMatchesUnique := make(map[string]struct{})
for _, keyMatch := range keyPat.FindAllStringSubmatch(dataStr, -1) {
keyMatchesUnique[strings.TrimSpace(keyMatch[1])] = struct{}{}
}
EndpointLoop:
for baseUrl := range urlMatchesUnique {
for key := range keyMatchesUnique {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AzureAPIManagementSubscriptionKey,
Raw: []byte(baseUrl),
RawV2: []byte(baseUrl + ":" + key),
}
if verify {
if invalidHosts.Exists(baseUrl) {
logger.V(3).Info("Skipping invalid registry", "baseUrl", baseUrl)
continue EndpointLoop
}
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := s.verifyMatch(ctx, client, baseUrl, key)
s1.Verified = isVerified
if verificationErr != nil {
if errors.Is(verificationErr, noSuchHostErr) {
invalidHosts.Set(baseUrl, struct{}{})
continue EndpointLoop
}
s1.SetVerificationError(verificationErr, baseUrl)
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureAPIManagementSubscriptionKey
}
func (s Scanner) Description() string {
return "Azure API Management provides a direct management REST API for performing operations on selected entities, such as users, groups, products, and subscriptions."
}
func (s Scanner) IsFalsePositive(_ detectors.Result) (bool, string) {
return false, ""
}
func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, baseUrl, key string) (bool, error) {
url := baseUrl + "/echo/resource" // default testing endpoint for api management services
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return false, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Ocp-Apim-Subscription-Key", key)
resp, err := client.Do(req)
if err != nil {
return false, nil
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/azureapimanagementsubscriptionkey/azureapimanagementsubscriptionkey_integration_test.go
================================================
//go:build detectors
// +build detectors
package azureapimanagementsubscriptionkey
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/detectorspb"
)
func TestAzureAPIManagementSubscriptionKey_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)
}
url := testSecrets.MustGetField("AZUREAPIMANAGEMENTSUBSCRIPTIONKEY_URL")
secret := testSecrets.MustGetField("AZUREDIRECTMANAGEMENTAPISUBSCRIPTIONKEY")
inactiveSecret := testSecrets.MustGetField("AZUREDIRECTMANAGEMENTAPISUBSCRIPTIONKEY_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",
s: Scanner{},
args: args{
ctx: ctx,
data: []byte(fmt.Sprintf("You can find a azure api management gateway url %s and subscription key %s within", url, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureAPIManagementSubscriptionKey,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: ctx,
data: []byte(fmt.Sprintf("You can find a azure api management gateway url %s and subscription key %s within but not valid", url, inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureAPIManagementSubscriptionKey,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "not found",
s: Scanner{},
args: args{
ctx: ctx,
data: []byte("You cannot find the secret within"),
verify: true,
},
want: nil,
wantErr: false,
wantVerificationErr: 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("AzureDirectManagementAPIKey.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "Redacted", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AzureDirectManagementAPIKey.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azureapimanagementsubscriptionkey/azureapimanagementsubscriptionkey_test.go
================================================
package azureapimanagementsubscriptionkey
import (
"context"
"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"
)
func TestAzureAPIManagementSubscriptionKey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
AZURE_API_MANAGEMENT_GATEWAY_URL=https://trufflesecuritytest.azure-api.net
AZURE_API_MANAGEMENT_SUBSCRIPTION_KEY=2c69j0dc327c4929b74d3a832a04266b
`,
want: []string{"https://trufflesecuritytest.azure-api.net:2c69j0dc327c4929b74d3a832a04266b"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{https://dffe5e2teoezcct050ch-2au74tmls8jm1p.azure-api.net}{AQAAABAAA uEDFd7-zSeH6dwwzLbGjVrAlfgXoV1Xv}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"https://dffe5e2teoezcct050ch-2au74tmls8jm1p.azure-api.net:uEDFd7-zSeH6dwwzLbGjVrAlfgXoV1Xv"},
},
{
name: "invalid pattern",
input: `
AZURE_API_MANAGEMENT_GATEWAY_URL=https://trufflesecuritytest.azure-api.net
AZURE_API_MANAGEMENT_SUBSCRIPTION_KEY=2c69j2dc3f7c4929b74d3a832a042
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/azureappconfigconnectionstring/azureappconfigconnectionstring.go
================================================
package azureappconfigconnectionstring
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
connectionStringPat = regexp.MustCompile(`Endpoint=(https:\/\/[a-zA-Z0-9-]+\.azconfig\.io);Id=([a-zA-Z0-9+\/=]+);Secret=([a-zA-Z0-9+\/=]+)`)
)
// 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{".azconfig.io"}
}
// FromData will find and optionally verify Azure Management API keys 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)
keyMatchesUnique := make(map[string][]string)
for _, keyMatch := range connectionStringPat.FindAllStringSubmatch(dataStr, -1) {
keyMatchesUnique[strings.TrimSpace(keyMatch[0])] = keyMatch // keep all the matched groups for verification
}
for connectionString, connectionInfo := range keyMatchesUnique {
endpoint := connectionInfo[1] // Endpoint
id := connectionInfo[2] // Id
secret := connectionInfo[3] // Secret
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AzureAppConfigConnectionString,
Raw: []byte(id),
RawV2: []byte(connectionString),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := s.verifyMatch(ctx, client, endpoint, id, secret)
s1.Verified = isVerified
if verificationErr != nil && !strings.Contains(verificationErr.Error(), "no such host") { // ignore no such host errors
s1.SetVerificationError(verificationErr, connectionString)
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureAppConfigConnectionString
}
func (s Scanner) Description() string {
return "Azure App Configuration is a managed service that centralizes application settings and feature flags, enabling dynamic updates without redeploying applications. Its connection string, which includes the endpoint URL and an access key, securely connects applications to the configuration store."
}
// generateHMACSignature creates the HMAC-SHA256 signature
func generateHMACSignature(secret, stringToSign string) (string, error) {
decodedSecret, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return "", fmt.Errorf("failed to decode secret: %w", err)
}
h := hmac.New(sha256.New, decodedSecret)
h.Write([]byte(stringToSign))
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
return signature, nil
}
// verifyMatch sends a request to the Azure App Configuration REST API to verify the provided credentials
// https://learn.microsoft.com/en-us/azure/azure-app-configuration/rest-api-authentication-hmac
func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, endpoint, id, secret string) (bool, error) {
apiVersion := "1.0"
requestPath := "/kv"
query := fmt.Sprintf("?api-version=%s", apiVersion)
url := fmt.Sprintf("%s%s%s", endpoint, requestPath, query)
// Prepare request
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return false, fmt.Errorf("failed to create request: %w", err)
}
// Set required headers
host := strings.TrimPrefix(strings.TrimPrefix(endpoint, "https://"), "http://")
date := time.Now().UTC().Format(http.TimeFormat)
contentHash := base64.StdEncoding.EncodeToString(sha256.New().Sum(nil)) // SHA256 hash of an empty body
req.Header.Set("Host", host)
req.Header.Set("Date", date)
req.Header.Set("x-ms-content-sha256", contentHash)
// Create the string to sign
stringToSign := fmt.Sprintf("%s\n%s%s\n%s;%s;%s",
http.MethodGet,
requestPath,
query,
date,
host,
contentHash,
)
// Generate the HMAC signature
signature, err := generateHMACSignature(secret, stringToSign)
if err != nil {
return false, fmt.Errorf("failed to generate HMAC signature: %w", err)
}
// Set the Authorization header
authorizationHeader := fmt.Sprintf(
"HMAC-SHA256 Credential=%s&SignedHeaders=date;host;x-ms-content-sha256&Signature=%s",
id,
signature,
)
req.Header.Set("Authorization", authorizationHeader)
// Send the request
resp, err := client.Do(req)
if err != nil {
return false, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
// Check the response status
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("got unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/azureappconfigconnectionstring/azureappconfigconnectionstring_integration_test.go
================================================
//go:build detectors
// +build detectors
package azureappconfigconnectionstring
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/detectorspb"
)
func TestAzureAppConfigConnectionString_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("AZURE_APP_CONFIGURATION_CONNECTION_STRING")
inactiveSecret := testSecrets.MustGetField("AZURE_APP_CONFIGURATION_CONNECTION_STRING_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",
s: Scanner{},
args: args{
ctx: ctx,
data: []byte(fmt.Sprintf("You can find a azureappconfigconnectionstring secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureAppConfigConnectionString,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: ctx,
data: []byte(fmt.Sprintf("You can find a azureappconfigconnectionstring secret %s but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureAppConfigConnectionString,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "not found",
s: Scanner{},
args: args{
ctx: ctx,
data: []byte("You cannot find the secret within"),
verify: true,
},
want: nil,
wantErr: false,
wantVerificationErr: 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("AzureAppConfigConnectionString.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "RawV2", "Raw", "Redacted", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AzureAppConfigConnectionString.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azureappconfigconnectionstring/azureappconfigconnectionstring_test.go
================================================
package azureappconfigconnectionstring
import (
"context"
"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"
)
func TestAzureAppConfigConnectionString_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `Endpoint=https://trufflesecurity.azconfig.io;Id=u+De;Secret=80DtxZkndXpM2mV2J1JjX2vL1x4gm1hHn8Y3JeFJ4N0PPLSO5D70JQQJ99BBAC1i4FpQkb5wAAACAAZC26dr`,
want: []string{"Endpoint=https://trufflesecurity.azconfig.io;Id=u+De;Secret=80DtxZkndXpM2mV2J1JjX2vL1x4gm1hHn8Y3JeFJ4N0PPLSO5D70JQQJ99BBAC1i4FpQkb5wAAACAAZC26dr"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{connectionstring}{AQAAABAAA Endpoint=https://iTHzRfnepCddRiYoBbPj-drVzUjwTNduwb3EUOTsuSAgg1e83Q7bw.azconfig.io;Id=eO04L+/m9rYn;Secret=G4jQ3GmcsYqlLkkG8uoIVbx08PZIJSdfB/7}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"Endpoint=https://iTHzRfnepCddRiYoBbPj-drVzUjwTNduwb3EUOTsuSAgg1e83Q7bw.azconfig.io;Id=eO04L+/m9rYn;Secret=G4jQ3GmcsYqlLkkG8uoIVbx08PZIJSdfB/7"},
},
{
name: "invalid pattern",
input: `Endpoint=https://trufflesecurity.azconfig.io;Secret=80DtxZkndXpMTmV2J3JjX2vL1x4gm1hHn8Y3KeFV4N0PPLSO5D70JQQJ79BBAC1i4FpRkb5wAAACAAZC26dr`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/azurecontainerregistry/azurecontainerregistry.go
================================================
package azurecontainerregistry
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
urlPat = regexp.MustCompile(`([a-z0-9][a-z0-9-]{1,100}[a-z0-9])\.azurecr\.io`)
passwordPat = regexp.MustCompile(`\b[a-zA-Z0-9+/]{42}\+ACR[a-zA-Z0-9]{6}\b`)
invalidHosts = simple.NewCache[struct{}]()
)
// 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{".azurecr.io"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureContainerRegistry
}
func (s Scanner) Description() string {
return "Azure's container registry is used to store docker containers. An API key can be used to override existing containers, read sensitive data, and do other operations inside the container registry."
}
// FromData will find and optionally verify Azurecontainerregistry secrets in a given set of bytes.
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
logger := logContext.AddLogger(ctx).Logger().WithName("azurecr")
dataStr := string(data)
// Deduplicate matches.
registryMatches := make(map[string]struct{})
for _, matches := range urlPat.FindAllStringSubmatch(dataStr, -1) {
u := matches[1]
// Ignore https://learn.microsoft.com/en-us/azure/container-registry/container-registry-private-link
if u == "privatelink" || u == "myacr" {
continue
}
registryMatches[u] = struct{}{}
}
passwordMatches := make(map[string]struct{})
for _, matches := range passwordPat.FindAllStringSubmatch(dataStr, -1) {
p := matches[0]
if detectors.StringShannonEntropy(p) < 4 {
continue
}
passwordMatches[p] = struct{}{}
}
EndpointLoop:
for username := range registryMatches {
for password := range passwordMatches {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_AzureContainerRegistry,
Raw: []byte(password),
RawV2: []byte(`{"username":"` + username + `","password":"` + password + `"}`),
Redacted: username,
}
if verify {
if invalidHosts.Exists(username) {
logger.V(3).Info("Skipping invalid registry", "username", username)
continue EndpointLoop
}
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyMatch(ctx, client, username, password)
if isVerified {
delete(passwordMatches, password)
r.Verified = true
}
if verificationErr != nil {
if errors.Is(verificationErr, noSuchHostErr) {
invalidHosts.Set(username, struct{}{})
continue EndpointLoop
}
r.SetVerificationError(verificationErr, password)
}
}
results = append(results, r)
if r.Verified {
break
}
}
}
return results, nil
}
func (s Scanner) IsFalsePositive(_ detectors.Result) (bool, string) {
return false, ""
}
var noSuchHostErr = errors.New("no such host")
func verifyMatch(ctx context.Context, client *http.Client, username string, password string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s.azurecr.io/v2/", username), nil)
if err != nil {
return false, err
}
req.SetBasicAuth(username, password)
res, err := client.Do(req)
if err != nil {
// lookup foo.azurecr.io: no such host
if strings.Contains(err.Error(), "no such host") {
return false, noSuchHostErr
}
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.StatusUnauthorized:
// The secret is determinately not verified.
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
================================================
FILE: pkg/detectors/azurecontainerregistry/azurecontainerregistry_integration_test.go
================================================
//go:build detectors
// +build detectors
package azurecontainerregistry
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAzureContainerRegistry_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)
}
azureHost := testSecrets.MustGetField("AZURE_CR_HOST")
password := testSecrets.MustGetField("AZURE_CR_PASSWORD")
passwordInactive := testSecrets.MustGetField("AZURE_CR_PASSWORD_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azurecontainerregistry secret %s and %s within", azureHost, password)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureContainerRegistry,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azurecontainerregistry secret %s and %s within but not valid", azureHost, passwordInactive)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureContainerRegistry,
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,
},
}
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("AzureContainerRegistry.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "RawV2", "Raw", "Redacted", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AzureContainerRegistry.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azurecontainerregistry/azurecontainerregistry_test.go
================================================
package azurecontainerregistry
import (
"context"
"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"
)
func TestAzureContainerRegistry_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "pwd",
input: `source storage.env
ACR=smpldev.azurecr.io
ACRUSER=smpldev
ACRPWD=Cw8xeDNK6Bub3p61aq5ij/TiVvtBicpTj5rverVezj+ACRBPkEcx
CONTAINER=storage-svc:latest`,
want: []string{`{"username":"smpldev","password":"Cw8xeDNK6Bub3p61aq5ij/TiVvtBicpTj5rverVezj+ACRBPkEcx"}`},
},
{
name: "password",
input: ` - name: Deploy to ARC
uses: azure/docker-login@v1
with:
login-server: crmshopacr.azurecr.io
username: crmshopacr
password: o9uXSjWlUdRwAeGP2xGSfGy+25vetsONo3Mq13fksa+ACRBXyFsY
- run: |`,
want: []string{`{"username":"crmshopacr","password":"o9uXSjWlUdRwAeGP2xGSfGy+25vetsONo3Mq13fksa+ACRBXyFsY"}`},
},
{
name: "docker cli login",
input: `docker login dvacr00.azurecr.io -u dvacr00 -p Ljc+1lq0U0+c3jHlMHxSxAhCipKt6zU43HfMle/Ymj+ACRAKcPHy
docker push dvacr00.azurecr.io/foo-alpine:3.18`,
want: []string{`{"username":"dvacr00","password":"Ljc+1lq0U0+c3jHlMHxSxAhCipKt6zU43HfMle/Ymj+ACRAKcPHy"}`},
},
{
name: "request body",
input: `"registries":[{"identity":"","passwordSecretRef":"registry-password","server":"cr2bxwtqgom2oo.azurecr.io","username":"cr2bxwtqgom2oo"}],"secrets":[{"name":"registry-password","value":"VP2rvkuld42mr3jNjM+rVbvIzVuZxwncKWyVU5UIad+ACRBivL0B"}]}`,
want: []string{`{"username":"cr2bxwtqgom2oo","password":"VP2rvkuld42mr3jNjM+rVbvIzVuZxwncKWyVU5UIad+ACRBivL0B"}`},
},
{
name: "README",
input: `# AZURE-CICD-Deployment-with-Github-Actions
## Save pass:
s3cEZKH3yytiVnJ3h+eI3qhhzf9l1vNwEi1+q+WGdd+ACRCZ7JD6
## Run from terminal:
docker build -t testapp.azurecr.io/chicken:latest .
`,
want: []string{`{"username":"testapp","password":"s3cEZKH3yytiVnJ3h+eI3qhhzf9l1vNwEi1+q+WGdd+ACRCZ7JD6"}`},
},
// TODO:
//{
// name: "az cli login",
// input: `az acr login --name tstcopilotacr --username tstcopilotacr --password 9iZkJiOTKeEsQDfgoobtCYU47EEDs9UvU4L8NErLV+ACRACptmc`,
// want: []string{},
//},
//{
// name: "",
// input: ``,
// want: []string{},
{
name: "invalid pattern",
input: `
azure:
url: http://invalid.azurecr.io.azure.com
secret: BXIMbhBlC3=5hIbqCEKvq7op!V2ZfO0XWbcnasZmPm/AJfQqdcnt/+2Ytxc1hDq1m/
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/azuredevopspersonalaccesstoken/azuredevopspersonalaccesstoken.go
================================================
package azuredevopspersonalaccesstoken
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure"}) + `\b([0-9a-z]{52})\b`)
orgPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure"}) + `\b([0-9a-zA-Z][0-9a-zA-Z-]{5,48}[0-9a-zA-Z])\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{"azure"}
}
// FromData will find and optionally verify AzureDevopsPersonalAccessToken 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
orgMatches := orgPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, orgMatch := range orgMatches {
resOrgMatch := strings.TrimSpace(orgMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AzureDevopsPersonalAccessToken,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resOrgMatch),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
req, err := http.NewRequestWithContext(ctx, "GET", "https://dev.azure.com/"+resOrgMatch+"/_apis/projects", nil)
if err != nil {
continue
}
req.SetBasicAuth("", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
hasVerifiedRes, _ := common.ResponseContainsSubstring(res.Body, "lastUpdateTime")
if res.StatusCode >= 200 && res.StatusCode < 300 && hasVerifiedRes {
s1.Verified = true
} else if res.StatusCode == 401 {
// The secret is determinately not verified (nothing to do)
} else {
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
s1.SetVerificationError(err, resMatch)
}
} else {
s1.SetVerificationError(err, resMatch)
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureDevopsPersonalAccessToken
}
func (s Scanner) Description() string {
return "Azure DevOps is a suite of development tools provided by Microsoft. Personal Access Tokens (PATs) are used to authenticate and authorize access to Azure DevOps services and resources."
}
================================================
FILE: pkg/detectors/azuredevopspersonalaccesstoken/azuredevopspersonalaccesstoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package azuredevopspersonalaccesstoken
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAzureDevopsPersonalAccessToken_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("AZURE_DEVOPS_PAT")
inactiveSecret := testSecrets.MustGetField("AZURE_DEVOPS_PAT_INACTIVE")
org := testSecrets.MustGetField("AZURE_DEVOPS_PAT_ORG")
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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azure secret %s azure organization %s within", secret, org)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureDevopsPersonalAccessToken,
Verified: true,
RawV2: []byte(secret + org),
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azure secret %s azure organization %s within but not valid", inactiveSecret, org)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureDevopsPersonalAccessToken,
Verified: false,
RawV2: []byte(inactiveSecret + org),
},
},
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 azure secret %s azure organization %s within", secret, org)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureDevopsPersonalAccessToken,
Verified: false,
RawV2: []byte(secret + org),
},
},
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 azure secret %s azure organization %s within", secret, org)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureDevopsPersonalAccessToken,
Verified: false,
RawV2: []byte(secret + org),
},
},
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("AzureDevopsPersonalAccessToken.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 len(got[i].RawV2) == 0 {
t.Fatalf("no rawV2 secret present: \n %+v", got[i])
}
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AzureDevopsPersonalAccessToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azuredevopspersonalaccesstoken/azuredevopspersonalaccesstoken_test.go
================================================
package azuredevopspersonalaccesstoken
import (
"context"
"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"
)
func TestAzureDevopsPersonalAccessToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
azure:
azure_key: uie5tff7m5h5lqnqjhaltetqli90a08p6dhv9rn59uo30jgzw8un
azure_org_id: WOkQXnjSxCyioEJRa8R6J39cN4Xfyy8CWl1BZksHYsevxVBFzG
`,
want: []string{"uie5tff7m5h5lqnqjhaltetqli90a08p6dhv9rn59uo30jgzw8unWOkQXnjSxCyioEJRa8R6J39cN4Xfyy8CWl1BZksHYsevxVBFzG"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{azure dlMR9GIBfqCgAPr8qfkBa072OfaP6NbBhCwkPBX0cuHd}{azure AQAAABAAA h0wpgbusyba8acyaec1uxxcbxlucgr490c6nvrvd8rylfocwkpg5}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{
"h0wpgbusyba8acyaec1uxxcbxlucgr490c6nvrvd8rylfocwkpg5dlMR9GIBfqCgAPr8qfkBa072OfaP6NbBhCwkPBX0cuHd",
"h0wpgbusyba8acyaec1uxxcbxlucgr490c6nvrvd8rylfocwkpg5AQAAABAAA",
},
},
{
name: "invalid pattern",
input: `
azure:
azure_key: uie5tff7m5H5lqnqjhaltetqli90a08p6dhv9rn59uo30jgzw8un
azure_org_id: LOKi
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/azuredirectmanagementkey/azuredirectmanagementkey.go
================================================
package azuredirectmanagementkey
import (
"context"
"crypto/hmac"
"crypto/sha512"
"encoding/base64"
"errors"
"fmt"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
const RFC3339WithoutMicroseconds = "2006-01-02T15:04:05"
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
urlPat = regexp.MustCompile(`https://([a-z0-9][a-z0-9-]{0,48}[a-z0-9])\.management\.azure-api\.net`) // https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.APIM.Name/
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure", ".management.azure-api.net"}) + `([a-zA-Z0-9+\/]{83,85}[a-zA-Z0-9]==)`) // pattern for both Primary and secondary key
invalidHosts = simple.NewCache[struct{}]()
noSuchHostErr = errors.New("no such host")
)
// 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{".management.azure-api.net"}
}
// FromData will find and optionally verify Azure Management API keys in a given set of bytes.
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
logger := logContext.AddLogger(ctx).Logger().WithName("azuredirectmanagementkey")
dataStr := string(data)
urlMatchesUnique := make(map[string]string)
for _, urlMatch := range urlPat.FindAllStringSubmatch(dataStr, -1) {
urlMatchesUnique[urlMatch[0]] = urlMatch[1] // urlMatch[0] is the full url, urlMatch[1] is the service name
}
keyMatchesUnique := make(map[string]struct{})
for _, keyMatch := range keyPat.FindAllStringSubmatch(dataStr, -1) {
keyMatchesUnique[strings.TrimSpace(keyMatch[1])] = struct{}{}
}
EndpointLoop:
for baseUrl, serviceName := range urlMatchesUnique {
for key := range keyMatchesUnique {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AzureDirectManagementKey,
Raw: []byte(baseUrl),
RawV2: []byte(baseUrl + ":" + key),
}
if verify {
if invalidHosts.Exists(baseUrl) {
logger.V(3).Info("Skipping invalid registry", "baseUrl", baseUrl)
continue EndpointLoop
}
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := s.verifyMatch(ctx, client, baseUrl, serviceName, key)
s1.Verified = isVerified
if verificationErr != nil {
if errors.Is(verificationErr, noSuchHostErr) {
invalidHosts.Set(baseUrl, struct{}{})
continue EndpointLoop
}
s1.SetVerificationError(verificationErr, baseUrl)
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureDirectManagementKey
}
func (s Scanner) Description() string {
return "Azure API Management provides a direct management REST API for performing operations on selected entities, such as users, groups, products, and subscriptions."
}
func (s Scanner) IsFalsePositive(_ detectors.Result) (bool, string) {
return false, ""
}
func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, baseUrl, serviceName, key string) (bool, error) {
url := fmt.Sprintf(
"%s/subscriptions/default/resourceGroups/default/providers/Microsoft.ApiManagement/service/%s/apis?api-version=2024-05-01",
baseUrl, serviceName,
)
accessToken, err := generateAccessToken(key)
if err != nil {
return false, err
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return false, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("SharedAccessSignature %s", accessToken))
resp, err := client.Do(req)
if err != nil {
return false, nil
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode)
}
}
// https://learn.microsoft.com/en-us/rest/api/apimanagement/apimanagementrest/azure-api-management-rest-api-authentication
func generateAccessToken(key string) (string, error) {
expiry := time.Now().UTC().Add(5 * time.Second).Format(RFC3339WithoutMicroseconds) // expire in 5 seconds
expiry = expiry + ".0000000Z" // 7 decimals microsecond's precision is must for access token
// Construct the string-to-sign
stringToSign := fmt.Sprintf("integration\n%s", expiry)
// Generate HMAC-SHA512 signature
h := hmac.New(sha512.New, []byte(key))
h.Write([]byte(stringToSign))
signature := h.Sum(nil)
// Base64 encode the signature
encodedSignature := base64.StdEncoding.EncodeToString(signature)
// Create the access token
accessToken := fmt.Sprintf("uid=integration&ex=%s&sn=%s", expiry, encodedSignature)
return accessToken, nil
}
================================================
FILE: pkg/detectors/azuredirectmanagementkey/azuredirectmanagementkey_integration_test.go
================================================
//go:build detectors
// +build detectors
package azuredirectmanagementkey
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/detectorspb"
)
func TestAzureDirectManagementAPIKey_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)
}
url := testSecrets.MustGetField("AZUREDIRECTMANAGEMENTAPI_URL")
secret := testSecrets.MustGetField("AZUREDIRECTMANAGEMENTAPI_KEY")
inactiveSecret := testSecrets.MustGetField("AZUREDIRECTMANAGEMENTAPI_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",
s: Scanner{},
args: args{
ctx: ctx,
data: []byte(fmt.Sprintf("You can find a azure management api url %s and key %s within", url, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureDirectManagementKey,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: ctx,
data: []byte(fmt.Sprintf("You can find a azure management api url %s and key %s within but not valid", url, inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureDirectManagementKey,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "not found",
s: Scanner{},
args: args{
ctx: ctx,
data: []byte("You cannot find the secret within"),
verify: true,
},
want: nil,
wantErr: false,
wantVerificationErr: 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("AzureDirectManagementAPIKey.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "Redacted", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AzureDirectManagementAPIKey.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azuredirectmanagementkey/azuredirectmanagementkey_test.go
================================================
package azuredirectmanagementkey
import (
"context"
"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"
)
func TestAzureDirectManagementAPIKey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
AZURE_MANGEMENT_API_KEY=UJh1Wn7txjls2GPK1YxO9+3tpqQffSfxb+97PmT8j3cSQoXvGa74lCKpBqPeppTHCharbaMeKqKs/H4gA/go1w==
AZURE_MANAGEMENT_API_URL=https://trufflesecuritytest.management.azure-api.net
`,
want: []string{"https://trufflesecuritytest.management.azure-api.net:UJh1Wn7txjls2GPK1YxO9+3tpqQffSfxb+97PmT8j3cSQoXvGa74lCKpBqPeppTHCharbaMeKqKs/H4gA/go1w=="},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{https://0q66287uqx.management.azure-api.net}{AQAAABAAA Ub4yMRDBBdEX/BNyNFM6i6Odj25TB0Zd1BRNx57ZeMGpqzkeokXheNpkkTBtvPQb692id65yc2xLKhZ183rg==}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"https://0q66287uqx.management.azure-api.net:Ub4yMRDBBdEX/BNyNFM6i6Odj25TB0Zd1BRNx57ZeMGpqzkeokXheNpkkTBtvPQb692id65yc2xLKhZ183rg=="},
},
{
name: "invalid pattern",
input: `
AZURE_MANGEMENT_API_KEY=UJh1Wn7txjls2GPK1YxO9+3tpqQffSfxb+97PmT8j3cSQoXvGa74lCKp
AZURE_MANAGEMENT_API_URL=https://trufflesecuritytest.management.azure-api.net
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/azurefunctionkey/azurefunctionkey.go
================================================
package azurefunctionkey
import (
"context"
"fmt"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure"}) + `\b([a-zA-Z0-9_-]{20,56})\b={0,2}`)
azureUrlPat = regexp.MustCompile(`\bhttps:\/\/([a-zA-Z0-9-]{2,30})\.azurewebsites\.net\/api\/([a-zA-Z0-9-]{2,30})\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{"azure"}
}
// FromData will find and optionally verify azure 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
urlMatches := azureUrlPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resTrim := strings.Split(strings.TrimSpace(match[0]), " ")
resMatch := resTrim[len(resTrim)-1]
for _, urlMatch := range urlMatches {
resUrl := strings.TrimSpace(urlMatch[0])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AzureFunctionKey,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resUrl),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
req, err := http.NewRequestWithContext(ctx, "GET", resUrl+"?code="+resMatch, nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
} else if res.StatusCode == 401 {
// The secret is determinately not verified (nothing to do)
} else {
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
s1.SetVerificationError(err, resMatch)
}
} else {
s1.SetVerificationError(err, resMatch)
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureFunctionKey
}
func (s Scanner) Description() string {
return "Azure Functions is a serverless compute service that lets you run event-triggered code without having to explicitly provision or manage infrastructure. Azure Function Keys can be used to access and manage these functions."
}
================================================
FILE: pkg/detectors/azurefunctionkey/azurefunctionkey_integration_test.go
================================================
//go:build detectors
// +build detectors
package azurefunctionkey
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAzureFunctionKey_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("AZURE_FUNCTION_KEY")
inactiveSecret := testSecrets.MustGetField("AZURE_FUNCTION_KEY_INACTIVE")
url := testSecrets.MustGetField("AZURE_FUNCTION_URL")
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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azure secret %s azure url %s", secret, url)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureFunctionKey,
Verified: true,
RawV2: []byte(secret + url),
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azure secret %s azure url %s but not valid", inactiveSecret, url)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureFunctionKey,
Verified: false,
RawV2: []byte(inactiveSecret + url),
},
},
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 azure secret %s azure url %s", secret, url)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureFunctionKey,
Verified: false,
RawV2: []byte(secret + url),
},
},
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 azure secret %s azure url %s", secret, url)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureFunctionKey,
Verified: false,
RawV2: []byte(secret + url),
},
},
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("AzureFunctionKey.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 len(got[i].RawV2) == 0 {
t.Fatalf("no rawV2 secret present: \n %+v", got[i])
}
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AzureFunctionKey.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azurefunctionkey/azurefunctionkey_test.go
================================================
package azurefunctionkey
import (
"context"
"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"
)
func TestAzureFunctionKey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
azure:
azureURL: https://z1dUSi5T.azurewebsites.net/api/W8anB5J4uQi-v3Dcd6p7ySE0E
azureFunctionkey: B8sm0KyfL1y8vPH3IDTdefevHBCGK33-=
`,
want: []string{
"azurewebsites.net/api/W8anB5J4uQi-v3Dcd6p7ySE0Ehttps://z1dUSi5T.azurewebsites.net/api/W8anB5J4uQi-v3Dcd6p7ySE0E",
"B8sm0KyfL1y8vPH3IDTdefevHBCGK33https://z1dUSi5T.azurewebsites.net/api/W8anB5J4uQi-v3Dcd6p7ySE0E",
},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{https://yaeuxPA9-H.azurewebsites.net/api/Hwy5K}{azure AQAAABAAA Ijbql3DKRyIZNQIddzCYKICr}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"Ijbql3DKRyIZNQIddzCYKICrhttps://yaeuxPA9-H.azurewebsites.net/api/Hwy5K"},
},
{
name: "invalid pattern",
input: `
azure:
azureURL: http://invalid.azurecr.io.azure.com
azureFunctionkey: BXIMbhBlC3=5hIbqCEKvq7op!V2ZfO0XWbcnasZmPm/AJfQqdcnt/+2Ytxc1hDq1m/
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/azuresastoken/azuresastoken.go
================================================
package azuresastoken
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// microsoft storage resource naming rules: https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules#microsoftstorage:~:text=format%3A%0AVaultName_KeyName_KeyVersion.-,Microsoft.Storage,-Expand%20table
urlPat = regexp.MustCompile(`https://([a-zA-Z0-9][a-z0-9_-]{1,22}[a-zA-Z0-9])\.blob\.core\.windows\.net/[a-z0-9]([a-z0-9-]{1,61}[a-z0-9])?(?:/[a-zA-Z0-9._-]+)*`)
keyPat = regexp.MustCompile(
detectors.PrefixRegex([]string{"azure", "sas", "token", "blob", ".blob.core.windows.net"}) +
`(sp=[racwdli]+&st=\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z&se=\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z(?:&sip=\d{1,3}(?:\.\d{1,3}){3}(?:-\d{1,3}(?:\.\d{1,3}){3})?)?(&spr=https)?(?:,https)?&sv=\d{4}-\d{2}-\d{2}&sr=[bcfso]&sig=[a-zA-Z0-9%]{10,})`)
invalidStorageAccounts = simple.NewCache[struct{}]()
noSuchHostErr = errors.New("no such host")
)
func (s Scanner) Keywords() []string {
return []string{
"azure",
".blob.core.windows.net",
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureSasToken
}
func (s Scanner) Description() string {
return "An Azure Shared Access Signature (SAS) token is a time-limited, permission-based URL query string that grants secure, granular access to Azure Storage resources (e.g., blobs, containers, files) without exposing account keys."
}
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
logger := logContext.AddLogger(ctx).Logger().WithName("azuresas")
dataStr := string(data)
// deduplicate urlMatches
urlMatchesUnique := make(map[string]string)
for _, urlMatch := range urlPat.FindAllStringSubmatch(dataStr, -1) {
urlMatchesUnique[urlMatch[0]] = urlMatch[1]
}
// deduplicate keyMatches
keyMatchesUnique := make(map[string]struct{})
for _, keyMatch := range keyPat.FindAllStringSubmatch(dataStr, -1) {
keyMatchesUnique[keyMatch[1]] = struct{}{}
}
// Check results.
UrlLoop:
for url, storageAccount := range urlMatchesUnique {
for key := range keyMatchesUnique {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AzureSasToken,
Raw: []byte(url),
RawV2: []byte(url + key),
}
if verify {
if invalidStorageAccounts.Exists(storageAccount) {
logger.V(3).Info("Skipping invalid storage account", "storage account", storageAccount)
break
}
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyMatch(ctx, client, url, key, true)
s1.Verified = isVerified
if verificationErr != nil {
if errors.Is(verificationErr, noSuchHostErr) {
invalidStorageAccounts.Set(storageAccount, struct{}{})
continue UrlLoop
}
s1.SetVerificationError(verificationErr, key)
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) IsFalsePositive(_ detectors.Result) (bool, string) {
return false, ""
}
func verifyMatch(ctx context.Context, client *http.Client, url, key string, retryOn403 bool) (bool, error) {
urlWithToken := url + "?" + key
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlWithToken, nil)
if err != nil {
return false, err
}
res, err := client.Do(req)
if err != nil {
if strings.Contains(err.Error(), "no such host") {
return false, noSuchHostErr
}
return false, err
}
defer res.Body.Close()
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
switch res.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusForbidden:
if retryOn403 && strings.Contains(string(bodyBytes), "Signature did not match") {
// need to add additional query parameters for container urls
// https://stackoverflow.com/questions/25038429/azure-shared-access-signature-signature-did-not-match
return verifyMatch(ctx, client, url, key+"&comp=list&restype=container", false)
}
if strings.Contains(string(bodyBytes), "AuthorizationFailure") && strings.Contains(key, "&sip=") {
return false, fmt.Errorf("SAS token is restricted to specific IP addresses")
}
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
================================================
FILE: pkg/detectors/azuresastoken/azuresastoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package azuresastoken
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/detectorspb"
)
func TestAzureSasToken_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)
}
url := testSecrets.MustGetField("AZURESASTOKEN_URL")
secret := testSecrets.MustGetField("AZURESASTOKEN")
inactiveSecret := testSecrets.MustGetField("AZURESASTOKEN_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azure sas url %s and token %s within", url, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureSasToken,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azure sas url %s and token %s within but not valid", url, inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureSasToken,
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,
},
}
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("AzureSasToken.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "Redacted", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AzureSasToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azuresastoken/azuresastoken_test.go
================================================
package azuresastoken
import (
"context"
"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"
)
func TestAzureSASToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
AZURE_BLOB_SAS_TOKEN=sp=r&st=2025-03-04T07:24:52Z&se=2025-04-04T15:24:52Z&spr=https&sv=2022-11-02&sr=c&sig=WSdF9YeZhvrbs%2B%2B1f8ZdDBzEe7fBJ%2BenuaXQ%2BJ9WOw0%3D
AZURE_BLOB_SAS_URL=https://trufflesecurity.blob.core.windows.net/trufflesecurity
`,
want: []string{"https://trufflesecurity.blob.core.windows.net/trufflesecuritysp=r&st=2025-03-04T07:24:52Z&se=2025-04-04T15:24:52Z&spr=https&sv=2022-11-02&sr=c&sig=WSdF9YeZhvrbs%2B%2B1f8ZdDBzEe7fBJ%2BenuaXQ%2BJ9WOw0%3D"},
},
{
name: "valid pattern with ip",
input: `
AZURE_BLOB_SAS_TOKEN=sp=rcwl&st=2025-03-10T06:58:25Z&se=2025-03-10T14:58:25Z&sip=168.1.6.50&spr=https&sv=2022-11-02&sr=c&sig=c%2BUXo%2FJwf%2FGHomqYaw6tyRykKMaAnyikkf8nS7btD3DYg%3D
AZURE_BLOB_SAS_URL=https://trufflesecurity.blob.core.windows.net/trufflesecurity
`,
want: []string{"https://trufflesecurity.blob.core.windows.net/trufflesecuritysp=rcwl&st=2025-03-10T06:58:25Z&se=2025-03-10T14:58:25Z&sip=168.1.6.50&spr=https&sv=2022-11-02&sr=c&sig=c%2BUXo%2FJwf%2FGHomqYaw6tyRykKMaAnyikkf8nS7btD3DYg%3D"},
},
{
name: "valid pattern with ip range",
input: `
AZURE_BLOB_SAS_TOKEN=sp=rcwl&st=2025-03-10T06:58:25Z&se=2025-03-10T14:58:25Z&sip=168.1.6.50-168.1.6.80&spr=https&sv=2022-11-02&sr=c&sig=RiA6rO2VwFNZ73trWyY6fsasg0ViUp0k3sDxcl6aA1Rtg%3D
AZURE_BLOB_SAS_URL=https://trufflesecurity.blob.core.windows.net/trufflesecurity
`,
want: []string{"https://trufflesecurity.blob.core.windows.net/trufflesecuritysp=rcwl&st=2025-03-10T06:58:25Z&se=2025-03-10T14:58:25Z&sip=168.1.6.50-168.1.6.80&spr=https&sv=2022-11-02&sr=c&sig=RiA6rO2VwFNZ73trWyY6fsasg0ViUp0k3sDxcl6aA1Rtg%3D"},
},
{
name: "valid pattern without https",
input: `
AZURE_BLOB_SAS_TOKEN=sp=rcwl&st=2025-03-10T06:58:25Z&se=2025-03-10T14:58:25Z&sv=2022-11-02&sr=c&sig=OYbYoPKW7vVGjFMBu2QDDW%2BlpoShcxawVHR91NQPosY8%3D
AZURE_BLOB_SAS_URL=https://trufflesecurity.blob.core.windows.net/trufflesecurity
`,
want: []string{"https://trufflesecurity.blob.core.windows.net/trufflesecuritysp=rcwl&st=2025-03-10T06:58:25Z&se=2025-03-10T14:58:25Z&sv=2022-11-02&sr=c&sig=OYbYoPKW7vVGjFMBu2QDDW%2BlpoShcxawVHR91NQPosY8%3D"},
},
{
name: "valid pattern with blob url",
input: `
AZURE_BLOB_SAS_TOKEN=sp=r&st=2025-03-04T07:24:52Z&se=2025-04-04T15:24:52Z&spr=https&sv=2022-11-02&sr=c&sig=WSdF9YeZhvrbs%2B%2B1f8ZdDBzEe7fBJ%2BenuaXQ%2BJ9WOw0%3D
AZURE_BLOB_SAS_URL=https://trufflesecurity.blob.core.windows.net/trufflesecurity/test_blob.txt
`,
want: []string{"https://trufflesecurity.blob.core.windows.net/trufflesecurity/test_blob.txtsp=r&st=2025-03-04T07:24:52Z&se=2025-04-04T15:24:52Z&spr=https&sv=2022-11-02&sr=c&sig=WSdF9YeZhvrbs%2B%2B1f8ZdDBzEe7fBJ%2BenuaXQ%2BJ9WOw0%3D"},
},
{
name: "invalid pattern",
input: `
AZURE_BLOB_SAS_TOKEN=st=2025-03-04T07:24:52Z&se=2025-04-04T15:24:52Z&spr=https&sv=2022-11-02&sr=c
AZURE_BLOB_SAS_URL=https://trufflesecurity.blob.core.windows.net/12trufflesecurity
`,
want: nil,
},
{
name: "invalid pattern with invalid permission",
input: `
AZURE_BLOB_SAS_TOKEN=sp=rqx&st=2025-03-04T07:24:52Z&se=2025-04-04T15:24:52Z&spr=https&sv=2022-11-02&sr=c&sig=WSdF9YeZhvrbs%2B%2B1f8ZdDBzEe7fBJ%2BenuaXQ%2BJ9WOw0%3D
AZURE_BLOB_SAS_URL=https://trufflesecurity.blob.core.windows.net/12trufflesecurity
`,
want: nil,
},
{
name: "invalid pattern with invalid ip",
input: `
AZURE_BLOB_SAS_TOKEN=sp=rcwl&st=2025-03-10T06:58:25Z&se=2025-03-10T14:58:25Z&sip=168.1.6&spr=https&sv=2022-11-02&sr=c&sig=c%2BUXo%2FJwf%2FGHomqYaw6tyRykKMaAnyikkf8nS7btD3DYg%3D
AZURE_BLOB_SAS_URL=https://trufflesecurity.blob.core.windows.net/trufflesecurity
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/azuresearchadminkey/azuresearchadminkey.go
================================================
package azuresearchadminkey
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure"}) + `\b([0-9a-zA-Z]{52})\b`)
servicePat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure"}) + `\b([0-9a-zA-Z]{7,40})\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{"azure"}
}
// FromData will find and optionally verify AzureSearchAdminKey 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
serviceMatches := servicePat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, serviceMatch := range serviceMatches {
resServiceMatch := strings.TrimSpace(serviceMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AzureSearchAdminKey,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resServiceMatch),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
req, err := http.NewRequestWithContext(ctx, "GET", "https://"+resServiceMatch+".search.windows.net/servicestats?api-version=2023-10-01-Preview", nil)
if err != nil {
continue
}
req.Header.Add("api-key", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
} else if res.StatusCode == 401 || res.StatusCode == 403 {
// The secret is determinately not verified (nothing to do)
} else {
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
s1.SetVerificationError(err, resMatch)
}
} else {
s1.SetVerificationError(err, resMatch)
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureSearchAdminKey
}
func (s Scanner) Description() string {
return "Azure Search is a search-as-a-service solution that allows developers to incorporate search capabilities into their applications. Azure Search Admin Keys can be used to manage and query search services."
}
================================================
FILE: pkg/detectors/azuresearchadminkey/azuresearchadminkey_integration_test.go
================================================
//go:build detectors
// +build detectors
package azuresearchadminkey
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAzureSearchAdminKey_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("AZURE_SEARCH_ADMIN_KEY")
inactiveSecret := testSecrets.MustGetField("AZURE_SEARCH_ADMIN_KEY_INACTIVE")
service := testSecrets.MustGetField("AZURE_SEARCH_ADMIN_KEY_SERVICE")
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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azure secret %s and azure service %s within", secret, service)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureSearchAdminKey,
Verified: true,
RawV2: []byte(secret + service),
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azure secret %s and azure service %s within but not valid", inactiveSecret, service)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureSearchAdminKey,
Verified: false,
RawV2: []byte(inactiveSecret + service),
},
},
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 azure secret %s and azure service %s within", secret, service)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureSearchAdminKey,
Verified: false,
RawV2: []byte(secret + service),
},
},
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 azure secret %s and azure service %s within", secret, service)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureSearchAdminKey,
Verified: false,
RawV2: []byte(secret + service),
},
},
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("AzureSearchAdminKey.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 len(got[i].RawV2) == 0 {
t.Fatalf("no rawV2 secret present: \n %+v", got[i])
}
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AzureSearchAdminKey.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azuresearchadminkey/azuresearchadminkey_test.go
================================================
package azuresearchadminkey
import (
"context"
"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"
)
func TestAzureSearchAdminKey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
azure:
azureKey: wRRPyhjv8m6JGRujUUrPKa8d3rJ0mrGAxhmqf3A68OgZmlWUJyma
azureService: TestingService01
`,
want: []string{"wRRPyhjv8m6JGRujUUrPKa8d3rJ0mrGAxhmqf3A68OgZmlWUJymaTestingService01", "wRRPyhjv8m6JGRujUUrPKa8d3rJ0mrGAxhmqf3A68OgZmlWUJymaazureKey"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{azure bhIIhGTLlW7gLxy4rM93gLPaPFwdRajJX}{azure AQAAABAAA Pntv3pDD31oczaYT99OanBBZyYlnKGUpQb4WEFnK6uUsKiR0Mc09}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{
"Pntv3pDD31oczaYT99OanBBZyYlnKGUpQb4WEFnK6uUsKiR0Mc09bhIIhGTLlW7gLxy4rM93gLPaPFwdRajJX",
"Pntv3pDD31oczaYT99OanBBZyYlnKGUpQb4WEFnK6uUsKiR0Mc09AQAAABAAA",
},
},
{
name: "invalid pattern",
input: `
azure:
Key: wRRPyhjv8m6JGRujUUr-PK#a8d3rJ0mrGAxhmqf3A68OgZmlWUJyma
Service: TS01
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/azuresearchquerykey/azuresearchquerykey.go
================================================
package azuresearchquerykey
import (
"context"
"fmt"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure"}) + `\b([0-9a-zA-Z]{52})\b`)
urlPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure"}) + `https:\/\/([0-9a-z]{5,40})\.search\.windows\.net\/indexes\/([0-9a-z]{5,40})\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{"azure"}
}
// FromData will find and optionally verify AzureSearchQueryKey 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
urlMatches := urlPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, urlMatch := range urlMatches {
resTrim := strings.Split(strings.TrimSpace(urlMatch[0]), " ")
resUrlMatch := resTrim[len(resTrim)-1]
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AzureSearchQueryKey,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resUrlMatch),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
req, err := http.NewRequestWithContext(ctx, "GET", resUrlMatch+"/docs/$count?api-version=2023-10-01-Preview", nil)
if err != nil {
continue
}
req.Header.Add("api-key", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
} else if res.StatusCode == 401 || res.StatusCode == 403 {
// The secret is determinately not verified (nothing to do)
} else {
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
s1.SetVerificationError(err, resMatch)
}
} else {
s1.SetVerificationError(err, resMatch)
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureSearchQueryKey
}
func (s Scanner) Description() string {
return "Azure Search Query Keys are used to authenticate search requests to Azure Search service. They should be kept confidential to prevent unauthorized access to search indexes and data."
}
================================================
FILE: pkg/detectors/azuresearchquerykey/azuresearchquerykey_integration_test.go
================================================
//go:build detectors
// +build detectors
package azuresearchquerykey
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestAzureSearchQueryKey_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("AZURE_SEARCH_QUERY_KEY")
inactiveSecret := testSecrets.MustGetField("AZURE_SEARCH_QUERY_KEY_INACTIVE")
url := testSecrets.MustGetField("AZURE_SEARCH_QUERY_KEY_URL")
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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azure secret %s and azure url %s within", secret, url)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureSearchQueryKey,
Verified: true,
RawV2: []byte(secret + url),
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a azure secret %s and azure url %s within but not valid", inactiveSecret, url)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureSearchQueryKey,
Verified: false,
RawV2: []byte(inactiveSecret + url),
},
},
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 azure secret %s and azure url %s within", secret, url)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureSearchQueryKey,
Verified: false,
RawV2: []byte(secret + url),
},
},
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 azure secret %s and azure url %s within", secret, url)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_AzureSearchQueryKey,
Verified: false,
RawV2: []byte(secret + url),
},
},
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("AzureSearchQueryKey.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 len(got[i].RawV2) == 0 {
t.Fatalf("no rawV2 secret present: \n %+v", got[i])
}
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AzureSearchQueryKey.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/azuresearchquerykey/azuresearchquerykey_test.go
================================================
package azuresearchquerykey
import (
"context"
"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"
)
func TestAzureSearchQueryKey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
azure:
azure_url: https://tzyexx2ktdfhha8w1cktqzbrgv37ywtu.search.windows.net/indexes/n81wg81jogjfq93cyxfi67vy2g7vwlcqfgi
azure_key: OKalbM5EBt5hloqU46phTUCZqvNAlZ4S2Jd2gFUCOQ3HG0vQ2uEp
`,
want: []string{"OKalbM5EBt5hloqU46phTUCZqvNAlZ4S2Jd2gFUCOQ3HG0vQ2uEphttps://tzyexx2ktdfhha8w1cktqzbrgv37ywtu.search.windows.net/indexes/n81wg81jogjfq93cyxfi67vy2g7vwlcqfgi"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{azure https://w3fsj4c22rdn7mhkf1yxbt7orrvzd720a.search.windows.net/indexes/5934qi40xctuhmzba7ty}{azure AQAAABAAA C3idqCYnGa1cTx7iEFJ684QCbSDcEz1jq4s7iRxDDPWYKoK3h3Lr}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"C3idqCYnGa1cTx7iEFJ684QCbSDcEz1jq4s7iRxDDPWYKoK3h3Lrhttps://w3fsj4c22rdn7mhkf1yxbt7orrvzd720a.search.windows.net/indexes/5934qi40xctuhmzba7ty"},
},
{
name: "invalid pattern",
input: `
azure:
url: http://invalid.azurecr.io.azure.com
azure_key: BXIMbhBlC3=5hIbqCEKvq7op!V2ZfO0XWbcnasZmPm/AJfQqdcnt/+2Ytxc1hDq1m/
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/bannerbear/v1/bannerbear.go
================================================
package bannerbear
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
func (s Scanner) Version() int { return 1 }
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bannerbear"}) + `\b([0-9a-zA-Z]{22}tt)\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{"bannerbear"}
}
// FromData will find and optionally verify Bannerbear 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Bannerbear,
Raw: []byte(resMatch),
ExtraData: map[string]string{
"version": fmt.Sprintf("%d", s.Version()),
},
}
if verify {
isVerified, verificationErr := verifyBannerBear(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Bannerbear
}
func (s Scanner) Description() string {
return "Bannerbear is an API for generating dynamic images, videos, and GIFs. Bannerbear API keys can be used to access and manipulate these resources."
}
// docs: https://developers.bannerbear.com/
func verifyBannerBear(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.bannerbear.com/v2/auth", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/bannerbear/v1/bannerbear_integration_test.go
================================================
//go:build detectors
// +build detectors
package bannerbear
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBannerbear_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BANNERBEAR")
inactiveSecret := testSecrets.MustGetField("BANNERBEAR_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bannerbear secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bannerbear,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bannerbear secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bannerbear,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Bannerbear.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])
}
got[i].Raw = nil
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "verificationError", "primarySecret", "ExtraData")
if diff := cmp.Diff(tt.want, got, ignoreOpts); diff != "" {
t.Errorf("BannerbearV1.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/bannerbear/v1/bannerbear_test.go
================================================
package bannerbear
import (
"context"
"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"
)
func TestBannerBear_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte("{}")))
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bannerBearToken := "Bearer yvxpthLIcYpZweFpPOVeCOtt"
req.Header.Set("Authorization", bannerBearToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"yvxpthLIcYpZweFpPOVeCOtt"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{bannerbear}{bannerbear AQAAABAAA Y5UbXOT1Xh1ZOCxztUvGqltt}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"Y5UbXOT1Xh1ZOCxztUvGqltt"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte("{}")))
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bannerBearToken := "Bearer yvxpthLIcYpZweFpPOVeCOtot"
req.Header.Set("Authorization", bannerBearToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/bannerbear/v2/bannerbear.go
================================================
package bannerbear
import (
"bytes"
"context"
"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/detectorspb"
)
type Scanner struct{}
func (s Scanner) Version() int { return 2 }
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(bb_(?:pr|ma)_[a-f0-9]{30})\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{"bb_pr_", "bb_ma_"}
}
// FromData will find and optionally verify Bannerbear 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
uniqueMatches := make(map[string]struct{}, len(matches))
for _, match := range matches {
uniqueMatches[match[1]] = struct{}{}
}
for match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Bannerbear,
Raw: []byte(match),
ExtraData: map[string]string{
"version": fmt.Sprintf("%d", s.Version()),
},
}
if verify {
isVerified, extraData, verificationErr := s.verifyBannerBear(ctx, client, match)
s1.Verified = isVerified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr, match)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Bannerbear
}
func (s Scanner) Description() string {
return "Bannerbear is an API for generating dynamic images, videos, and GIFs. Bannerbear API keys can be used to access and manipulate these resources."
}
// docs: https://developers.bannerbear.com/
func (s Scanner) verifyBannerBear(ctx context.Context, client *http.Client, key string) (bool, map[string]string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.bannerbear.com/v2/auth", http.NoBody)
if err != nil {
return false, nil, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
resp, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
extraData := map[string]string{"version": fmt.Sprintf("%d", s.Version())}
switch resp.StatusCode {
case http.StatusOK:
extraData["key_type"] = "Project API Key"
return true, extraData, nil
case http.StatusBadRequest:
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return false, extraData, err
}
// According to Bannerbear API docs (https://developers.bannerbear.com/#authentication), the /auth endpoint
// expects us to add a project_id parameter to the payload, when using a Full Access Master API Key.
// otherwise, it returns a 400 Bad Request with "Error: When using a Master API Key you must set a project_id parameter"
// Also, when we use a Master API Key with limited access, it returns a 400 Bad Request with "Error: this Master Key is Limited Access only"
validResponse := bytes.Contains(bodyBytes, []byte("When using a Master API Key")) || bytes.Contains(bodyBytes, []byte("Master Key is Limited Access"))
if validResponse {
extraData["key_type"] = "Master API Key"
return true, extraData, nil
} else {
return false, extraData, fmt.Errorf("bad request: %s, body: %s", resp.Status, string(bodyBytes))
}
case http.StatusUnauthorized:
return false, extraData, nil
default:
return false, extraData, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/bannerbear/v2/bannerbear_integration_test.go
================================================
//go:build detectors
// +build detectors
package bannerbear
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBannerbear_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("BANNERBEARV2")
inactiveSecret := testSecrets.MustGetField("BANNERBEARV2_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bannerbear secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bannerbear,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bannerbear secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bannerbear,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Bannerbear.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])
}
got[i].Raw = nil
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "verificationError", "primarySecret", "ExtraData")
if diff := cmp.Diff(tt.want, got, ignoreOpts); diff != "" {
t.Errorf("BannerbearV2.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/bannerbear/v2/bannerbear_test.go
================================================
package bannerbear
import (
"context"
"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"
)
func TestBannerBear_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte("{}")))
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bannerBearToken := "Bearer bb_pr_abcdc2b40ef44abcd8cbf3739aabcd"
req.Header.Set("Authorization", bannerBearToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"bb_pr_abcdc2b40ef44abcd8cbf3739aabcd"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{}{AQAAABAAA bb_ma_900063380acef4c7e24c5bcee8af22}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"bb_ma_900063380acef4c7e24c5bcee8af22"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte("{}")))
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bannerBearToken := "Bearer bb_ma_abcdc2b40ef44abcd8cbf3739aabcq"
req.Header.Set("Authorization", bannerBearToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/baremetrics/baremetrics.go
================================================
package baremetrics
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
/*
Baremetrics has two type of keys:
- Sandbox: starts with `sk_`
- Production: starts with `lk_`
The length of key is not fixed and can range between 18 to 25 characters.
*/
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"baremetrics"}) + `\b((?:sk|lk)_[a-zA-Z0-9]{18,25})\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{"baremetrics"}
}
// FromData will find and optionally verify Baremetrics 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Baremetrics,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBaremetrics(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Baremetrics
}
func (s Scanner) Description() string {
return "Baremetrics is a subscription analytics and insights tool. Baremetrics API keys can be used to access and analyze subscription data."
}
// docs: https://developers.baremetrics.com/reference/authentication
func verifyBaremetrics(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.baremetrics.com/v1/account", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
resp, err := client.Do(req)
if err != nil {
return false, nil
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/baremetrics/baremetrics_integration_test.go
================================================
//go:build detectors
// +build detectors
package baremetrics
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBaremetrics_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BAREMETRICS")
inactiveSecret := testSecrets.MustGetField("BAREMETRICS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a baremetrics secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Baremetrics,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a baremetrics secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Baremetrics,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Baremetrics.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Baremetrics.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/baremetrics/baremetrics_test.go
================================================
package baremetrics
import (
"context"
"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"
)
func TestBareMetrics_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
baremetricsToken := "Bearer sk_nGDJWCkPiFAKE5XFTzUUA"
req.Header.Set("Authorization", baremetricsToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"sk_nGDJWCkPiFAKE5XFTzUUA"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{baremetrics}{baremetrics AQAAABAAA lk_JcWYJEi80ZzQA1nRXD}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"lk_JcWYJEi80ZzQA1nRXD"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
baremetricsToken := "Bearer sk_nGDJWC_io8Q025XFTzUUA"
req.Header.Set("Authorization", baremetricsToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/beamer/beamer.go
================================================
package beamer
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"beamer"}) + `\b([a-zA-Z0-9_+/]{45}=)`)
)
// 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{"beamer"}
}
// FromData will find and optionally verify Beamer 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Beamer,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBeamer(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Beamer
}
func (s Scanner) Description() string {
return "Beamer is a user engagement platform that helps you communicate product updates and other important information to your users. Beamer API keys can be used to access and manage this information."
}
func verifyBeamer(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.getbeamer.com/v0/url", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Beamer-Api-Key", key)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/beamer/beamer_integration_test.go
================================================
//go:build detectors
// +build detectors
package beamer
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBeamer_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BEAMER_TOKEN")
inactiveSecret := testSecrets.MustGetField("BEAMER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a beamer secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Beamer,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a beamer secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Beamer,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Beamer.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Beamer.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/beamer/beamer_test.go
================================================
package beamer
import (
"context"
"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"
)
func TestBeamer_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("Beamer-Api-Key", "DyVdf7+cAXw4MH9gT1CPotU31RMl__aLKbrABRWvT7TyO=")
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"DyVdf7+cAXw4MH9gT1CPotU31RMl__aLKbrABRWvT7TyO="},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{beamer}{beamer AQAAABAAA _FXYx2kyyNv6n_CBb9LrMHZPXa_S8iaj89zYn9mICmkB4=}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"_FXYx2kyyNv6n_CBb9LrMHZPXa_S8iaj89zYn9mICmkB4="},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("Beamer-Api-Key", "DyVdf7%c^AXw4MH9gT1CPotU31RMl__aLKbrABRWvT7TyO")
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/beebole/beebole.go
================================================
package beebole
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"beebole"}) + `\b([0-9a-z]{40})\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{"beebole"}
}
// FromData will find and optionally verify Beebole 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Beebole,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBeebole(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Beebole
}
func (s Scanner) Description() string {
return "Beebole is a time tracking and business management tool. Beebole API keys can be used to access and manage time tracking data and other business-related information."
}
// docs: https://beebole.com/help/api/
func verifyBeebole(ctx context.Context, client *http.Client, key string) (bool, error) {
payload := strings.NewReader(`{"service": "custom_field.list"}`)
req, err := http.NewRequestWithContext(ctx, "POST", "https://beebole-apps.com/api/v2", payload)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.SetBasicAuth(key, "x")
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/beebole/beebole_integration_test.go
================================================
//go:build detectors
// +build detectors
package beebole
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBeebole_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BEEBOLE")
inactiveSecret := testSecrets.MustGetField("BEEBOLE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a beebole secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Beebole,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a beebole secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Beebole,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Beebole.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Beebole.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/beebole/beebole_test.go
================================================
package beebole
import (
"context"
"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"
)
func TestBeeBole_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
beeboleAuth := bn6htprmfpukfalts4muwalxh9j15ucvnrfdme8t
req.Header.Set("Authorization", "Basic " + beeboleAuth) // beebole authorization header
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"bn6htprmfpukfalts4muwalxh9j15ucvnrfdme8t"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{beebole}{beebole AQAAABAAA rtwtgvvvekkik48t08tvf659hvyb5w8u4xnueh3u}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"rtwtgvvvekkik48t08tvf659hvyb5w8u4xnueh3u"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
beeboleAuth := DyVdf7%c^AXw4MH9gT1CPotU31RMl__aLKbrABRWvT7TyO
req.Header.Set("Authorization", "Basic " + beeboleAuth) // beebole authorization header
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/besnappy/besnappy.go
================================================
package besnappy
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"besnappy"}) + `\b([a-f0-9]{64})\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{"besnappy"}
}
// FromData will find and optionally verify Besnappy 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Besnappy,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBesnappy(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Besnappy
}
func (s Scanner) Description() string {
return "Besnappy is a customer service platform. The detected key can be used to access Besnappy's API, potentially exposing sensitive customer service data."
}
// docs: https://github.com/BeSnappy/api-docs
func verifyBesnappy(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://app.besnappy.com/api/v1/accounts", http.NoBody)
if err != nil {
return false, err
}
req.SetBasicAuth(key, "x")
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/besnappy/besnappy_integration_test.go
================================================
//go:build detectors
// +build detectors
package besnappy
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBesnappy_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BESNAPPY")
inactiveSecret := testSecrets.MustGetField("BESNAPPY_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a besnappy secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Besnappy,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a besnappy secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Besnappy,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Besnappy.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Besnappy.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/besnappy/besnappy_test.go
================================================
package besnappy
import (
"context"
"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"
)
func TestBeSnappy_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte("{}")))
if err != nil {
fmt.Println("Error creating request:", err)
return
}
beSnappyToken := f58c5d37d7876d32cfdd823f8fe4ded364a8d483b5dbfadcc55ad801b3be8523
req.Header.Set("Authorization", "Basic " + beSnappyToken) // authorization header
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"f58c5d37d7876d32cfdd823f8fe4ded364a8d483b5dbfadcc55ad801b3be8523"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{besnappy}{besnappy AQAAABAAA da5a2e65d83a40d6cebaac60ef01803f8c1a612baa428992ad4c7301df2759ba}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"da5a2e65d83a40d6cebaac60ef01803f8c1a612baa428992ad4c7301df2759ba"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte("{}")))
if err != nil {
fmt.Println("Error creating request:", err)
return
}
beSnappyToken := f58c5d37d7876d32cf__f8fe4ded364a8d483b5db+adcc55ad801b3be8523
req.Header.Set("Authorization", "Basic " + beSnappyToken) // authorization header
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/besttime/besttime.go
================================================
package besttime
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"besttime"}) + `\b(pri_[a-f0-9]{32})\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{"besttime"}
}
// FromData will find and optionally verify Besttime 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Besttime,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBesttime(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Besttime
}
func (s Scanner) Description() string {
return "Besttime is a service used to predict the best time to visit a place. Besttime API keys can be used to access and utilize this service."
}
// docs: https://documentation.besttime.app/#api-reference
func verifyBesttime(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://besttime.app/api/v1/keys/"+key, nil)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return false, err
}
body := string(bodyBytes)
if strings.Contains(body, `"status": "OK"`) {
return true, nil
} else if strings.Contains(body, `"message": "Invalid api_key_private`) {
return false, nil
}
return false, fmt.Errorf("unexpected response body: %s", body)
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/besttime/besttime_integration_test.go
================================================
//go:build detectors
// +build detectors
package besttime
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBesttime_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BESTTIME")
inactiveSecret := testSecrets.MustGetField("BESTTIME_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a besttime secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Besttime,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a besttime secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Besttime,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Besttime.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Besttime.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/besttime/besttime_test.go
================================================
package besttime
import (
"context"
"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"
)
func TestBestTime_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/besttime/keys/pri_099889f14d114dfaae476569b395eade"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"pri_099889f14d114dfaae476569b395eade"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{besttime}{besttime AQAAABAAA pri_cffe0fa1b281feeb01216ec73e149b00}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"pri_cffe0fa1b281feeb01216ec73e149b00"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/besttime/keys/4K1WTb2ysVeg^jHD*wtwhH68K9MuOjiTtXQCS"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/betterstack/betterstack.go
================================================
package betterstack
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"betterstack"}) + `\b([0-9a-zA-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{"betterstack"}
}
// FromData will find and optionally verify Betterstack 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_BetterStack,
Raw: []byte(resMatch),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyBetterStack(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_BetterStack
}
func (s Scanner) Description() string {
return "Betterstack is a monitoring service for uptime and performance of websites and APIs. Betterstack API keys can be used to access and manage these monitoring services."
}
// docs: https://betterstack.com/docs/uptime/api/list-all-existing-monitors/
func verifyBetterStack(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://uptime.betterstack.com/api/v2/monitors", nil)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/betterstack/betterstack_integration_test.go
================================================
//go:build detectors
// +build detectors
package betterstack
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBetterstack_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("BETTERSTACK")
inactiveSecret := testSecrets.MustGetField("BETTERSTACK_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a betterstack secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BetterStack,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a betterstack secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BetterStack,
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,
},
}
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("Betterstack.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Betterstack.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/betterstack/betterstack_test.go
================================================
package betterstack
import (
"context"
"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"
)
func TestBetterStack_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("Authorization", "Bearer " + getbetterStackToken())
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
func getBetterStackToken() string{ return "ntJD0ER8QpuT0O1WqsclApO2" }
`,
want: []string{"ntJD0ER8QpuT0O1WqsclApO2"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{betterstack}{betterstack AQAAABAAA RtSmhl4GkEcFS84Oyi0zlYbE}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"RtSmhl4GkEcFS84Oyi0zlYbE"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("Authorization", "Bearer " + getbetterStackToken())
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
func getBetterStackToken() string{ return "DyntJD0ER8QpuT0O1WqsclApO2" }
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/billomat/billomat.go
================================================
package billomat
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"billomat"}) + `\b([0-9a-z]{4,20})\b`) // the Billomat ID must be between 4 and 20 characters long.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"billomat"}) + `\b([0-9a-f]{32})\b`)
errAccountIDNotFound = errors.New("account id not found")
)
// 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{"billomat"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Billomat
}
func (s Scanner) Description() string {
return "Billomat is an online invoicing software. Billomat API keys can be used to access and manage invoices, clients, and other related data."
}
// FromData will find and optionally verify Billomat 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)
var uniqueIDs, uniqueAPIKeys = make(map[string]struct{}), make(map[string]struct{})
for _, match := range idPat.FindAllStringSubmatch(dataStr, -1) {
uniqueIDs[match[1]] = struct{}{}
}
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueAPIKeys[match[1]] = struct{}{}
}
for apiKey := range uniqueAPIKeys {
for id := range uniqueIDs {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Billomat,
Raw: []byte(apiKey),
RawV2: []byte(apiKey + id),
}
if verify {
isVerified, verificationErr := verifyBillomat(ctx, client, id, apiKey)
s1.Verified = isVerified
if verificationErr != nil {
// remove the account ID if not found to prevent reuse during other API key checks.
if errors.Is(verificationErr, errAccountIDNotFound) {
delete(uniqueIDs, id)
continue
}
s1.SetVerificationError(verificationErr, apiKey)
}
}
results = append(results, s1)
}
}
return results, nil
}
// docs: https://www.billomat.com/en/api/basics/authentication/
func verifyBillomat(ctx context.Context, client *http.Client, id, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s.billomat.net/api/v2/clients/myself", id), http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-BillomatApiKey", key)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
case http.StatusNotFound: // billomat api returns 404 if account id does not exist
// read the full response body
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return false, nil
}
/*
The regex for capturing a Billomat ID is prone to false positives.
To minimize incorrect matches, we return an error if the captured account ID does not exist,
as this likely indicates the match was invalid.
*/
if strings.Contains(string(bodyBytes), "account not found") {
return false, errAccountIDNotFound
}
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/billomat/billomat_integration_test.go
================================================
//go:build detectors
// +build detectors
package billomat
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBillomat_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BILLOMAT")
id := testSecrets.MustGetField("BILLOMAT_ID")
inactiveSecret := testSecrets.MustGetField("BILLOMAT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a billomat secret %s within billomat id %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Billomat,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a billomat secret %s within billomat id %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Billomat,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Billomat.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])
}
got[i].Raw = nil
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Billomat.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/billomat/billomat_test.go
================================================
package billomat
import (
"context"
"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"
)
func TestBilloMat_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.billomat.net/v2/id/truffletest"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("X-BillomatApiKey", "c09761f99f39f79ae28eaaf8df20d7c9")
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
// Check response status
if resp.StatusCode == http.StatusOK {
fmt.Println("Request successful!")
} else {
fmt.Println("Request failed with status:", resp.Status)
}
}`,
want: []string{
"c09761f99f39f79ae28eaaf8df20d7c9truffletest",
},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{billomat m2o8fqf8}{billomat AQAAABAAA 36a584c280b5b617e8eb25dae6b64d63}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"36a584c280b5b617e8eb25dae6b64d63m2o8fqf8"},
},
{
name: "invalid pattern",
input: `
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("X-BillomatApiKey", "c09761h99f39f79ae28eaaf8df20d7c9")
billomatID := truffle-test
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/bingsubscriptionkey/bingsubscriptionkey.go
================================================
package bingsubscriptionkey
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bing"}) + `\b([a-fA-F0-9]{32})\b`)
)
func (s Scanner) Keywords() []string {
return []string{"bing"}
}
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 match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_BingSubscriptionKey,
Raw: []byte(match),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyMatch(ctx, client, match)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, match)
}
results = append(results, s1)
}
return
}
func verifyMatch(ctx context.Context, client *http.Client, subscriptionKey string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.bing.microsoft.com/v7.0/search?q=trufflehog", nil)
if err != nil {
return false, err
}
req.Header.Add("Ocp-Apim-Subscription-Key", subscriptionKey)
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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_BingSubscriptionKey
}
func (s Scanner) Description() string {
return "Bing Subscription Key is a key used to access the Bing Web Search API."
}
================================================
FILE: pkg/detectors/bingsubscriptionkey/bingsubscriptionkey_integration_test.go
================================================
//go:build detectors
// +build detectors
package bingsubscriptionkey
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/detectorspb"
)
func TestBingsubscriptionkey_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("BING_SUBSCRIPTION_KEY")
inactiveSecret := testSecrets.MustGetField("BING_SUBSCRIPTION_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bing subscription key %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BingSubscriptionKey,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bing subscription key %s within but not valid", inactiveSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BingSubscriptionKey,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "not found",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte("You cannot find the key 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 bing subscription key %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BingSubscriptionKey,
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 bing subscription key %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BingSubscriptionKey,
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("Bingsubscriptionkey.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Bingsubscriptionkey.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/bingsubscriptionkey/bingsubscriptionkey_test.go
================================================
package bingsubscriptionkey
import (
"context"
"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"
)
func TestBingsubscriptionkey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.net/v2/api"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// set bing subscription key
bingKey := "89017d414ed64edb9c776d4a52102b9a"
req.Header.Set("Ocp-Apim-Subscription-Key", bingKey)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}`,
want: []string{"89017d414ed64edb9c776d4a52102b9a"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{bing}{bing AQAAABAAA dB963b030A1DafB02d8299F04A00a306}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"dB963b030A1DafB02d8299F04A00a306"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.net/v2/api"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// set bing subscription key
bingKey := "89017d414ed64edb9c776d4J52102b9"
req.Header.Set("Ocp-Apim-Subscription-Key", bingKey)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}`,
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("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/bitbar/bitbar.go
================================================
package bitbar
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bitbar"}) + `\b([0-9a-zA-Z]{32})\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{"bitbar"}
}
// FromData will find and optionally verify Bitbar 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Bitbar,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBitBar(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Bitbar
}
func (s Scanner) Description() string {
return "Bitbar provides a cloud-based mobile app testing platform. Bitbar API keys can be used to access and manage testing resources and data."
}
// docs: https://support.smartbear.com/bitbar/docs/en/use-rest-apis-with-bitbar.html
func verifyBitBar(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://cloud.bitbar.com/api/me", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.SetBasicAuth(key, "")
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/bitbar/bitbar_integration_test.go
================================================
//go:build detectors
// +build detectors
package bitbar
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBitbar_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BITBAR")
inactiveSecret := testSecrets.MustGetField("BITBAR_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bitbar secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bitbar,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bitbar secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bitbar,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Bitbar.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Bitbar.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/bitbar/bitbar_test.go
================================================
package bitbar
import (
"context"
"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"
)
func TestBitBar_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bitBarSecret := os.GetEnv("BITBAR_SECRET")
if bitBarSecret == ""{
bitBarSecret = "64pq66z15thg8fh3acd00l35lpyg7c82"
}
req.Header.Set("Authorization", "Basic " + bitBarSecret)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"64pq66z15thg8fh3acd00l35lpyg7c82"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{bitbar}{bitbar AQAAABAAA EJEpftl3MtqwEvE9nwiJhw2rWgjrhP1q}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"EJEpftl3MtqwEvE9nwiJhw2rWgjrhP1q"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bitBarSecret := os.GetEnv("BITBAR_SECRET")
if bitBarSecret == ""{
bitBarSecret = "DyV64pq66z15thg8fh3&cd00l35lpyg7c82$"
}
req.Header.Set("Authorization", "Basic " + bitBarSecret)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/bitbucketapppassword/bitbucketapppassword.go
================================================
package bitbucketapppassword
import (
"context"
"encoding/base64"
"fmt"
"io"
"net/http"
"regexp"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
// Scanner is a stateless struct that implements the detector interface.
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
// Keywords are used for efficiently pre-filtering chunks.
func (s Scanner) Keywords() []string {
return []string{"bitbucket", "ATBB"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_BitbucketAppPassword
}
func (s Scanner) Description() string {
return "Bitbucket is a Git repository hosting service by Atlassian. Bitbucket App Passwords are used to authenticate to the Bitbucket API."
}
const bitbucketAPIUserURL = "https://api.bitbucket.org/2.0/user"
var (
defaultClient = common.SaneHttpClient()
)
var (
// credentialPatterns uses named capture groups (?P...) for readability and robustness.
credentialPatterns = []*regexp.Regexp{
// Explicitly define the boundary as (start of string) or (a non-username character).
regexp.MustCompile(`(?:^|[^A-Za-z0-9-_])(?P[A-Za-z0-9-_]{1,30}):(?PATBB[A-Za-z0-9_=.-]+)\b`),
// Catches 'https://username:password@bitbucket.org' pattern
regexp.MustCompile(`https://(?P[A-Za-z0-9-_]{1,30}):(?PATBB[A-Za-z0-9_=.-]+)@bitbucket\.org`),
// Catches '("username", "password")' pattern, used for HTTP Basic Auth
regexp.MustCompile(`"(?P[A-Za-z0-9-_]{1,30})",\s*"(?PATBB[A-Za-z0-9_=.-]+)"`),
}
)
// FromData will find and optionally verify Bitbucket App Password secrets in a given set of bytes.
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) ([]detectors.Result, error) {
dataStr := string(data)
uniqueCredentials := make(map[string]string)
for _, pattern := range credentialPatterns {
for _, match := range pattern.FindAllStringSubmatch(dataStr, -1) {
// Extract credentials using named capture groups for readability.
namedMatches := make(map[string]string)
for i, name := range pattern.SubexpNames() {
if i != 0 && name != "" {
namedMatches[name] = match[i]
}
}
username := namedMatches["username"]
password := namedMatches["password"]
if username != "" && password != "" {
uniqueCredentials[username] = password
}
}
}
var results []detectors.Result
for username, password := range uniqueCredentials {
result := detectors.Result{
DetectorType: detectorspb.DetectorType_BitbucketAppPassword,
Raw: fmt.Appendf(nil, "%s:%s", username, password),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
var vErr error
result.Verified, vErr = verifyCredential(ctx, client, username, password)
if vErr != nil {
result.SetVerificationError(vErr, username, password)
}
}
results = append(results, result)
}
return results, nil
}
// verifyCredential checks if a given username and app password are valid by making a request to the Bitbucket API.
func verifyCredential(ctx context.Context, client *http.Client, username, password string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, bitbucketAPIUserURL, nil)
if err != nil {
return false, err
}
req.Header.Add("Accept", "application/json")
auth := base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", username, password))
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", auth))
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, http.StatusForbidden:
// A 403 can indicate a valid credential with insufficient scope, which is still a finding.
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
================================================
FILE: pkg/detectors/bitbucketapppassword/bitbucketapppassword_integration_test.go
================================================
//go:build detectors
// +build detectors
package bitbucketapppassword
import (
"context"
"fmt"
"testing"
"time"
"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/pb/detectorspb"
)
func TestBitbucketAppPassword_FromData_Integration(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
username := testSecrets.MustGetField("USERNAME")
validPassword := testSecrets.MustGetField("BITBUCKETAPPPASSWORD")
invalidPassword := "ATBB123abcDEF456ghiJKL789mnoPQR" // An invalid but correctly formatted password
tests := []struct {
name string
input string
want []detectors.Result
wantErr bool
}{
{
name: "valid credential",
input: fmt.Sprintf("https://%s:%s@bitbucket.org", username, validPassword),
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BitbucketAppPassword,
Verified: true,
Raw: []byte(fmt.Sprintf("%s:%s", username, validPassword)),
},
},
},
{
name: "invalid credential",
input: fmt.Sprintf("https://%s:%s@bitbucket.org", username, invalidPassword),
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BitbucketAppPassword,
Verified: false,
Raw: []byte(fmt.Sprintf("%s:%s", username, invalidPassword)),
},
},
},
{
name: "no credential found",
input: "this string has no credentials",
want: nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
s := &Scanner{}
got, err := s.FromData(ctx, true, []byte(tc.input))
if (err != nil) != tc.wantErr {
t.Fatalf("FromData() error = %v, wantErr %v", err, tc.wantErr)
}
// Normalizing results for comparison by removing fields that are not relevant for the test
for i := range got {
if got[i].VerificationError() != nil {
t.Logf("verification error: %s", got[i].VerificationError())
}
}
if diff := cmp.Diff(tc.want, got, cmp.Comparer(func(x, y detectors.Result) bool {
return x.Verified == y.Verified && string(x.Raw) == string(y.Raw) && x.DetectorType == y.DetectorType
})); diff != "" {
t.Errorf("FromData() mismatch (-want +got):\n%s", diff)
}
})
}
}
func BenchmarkFromData(benchmark *testing.B) {
ctx := context.Background()
s := &Scanner{}
for name, data := range detectors.MustGetBenchmarkData() {
benchmark.Run(name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, err := s.FromData(ctx, false, data)
if err != nil {
b.Fatal(err)
}
}
})
}
}
================================================
FILE: pkg/detectors/bitbucketapppassword/bitbucketapppassword_test.go
================================================
package bitbucketapppassword
import (
"context"
"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"
)
func TestBitbucketAppPassword_FromData(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pair",
input: `
[INFO] Sending request to the bitbucket API
[DEBUG] Using autodesk Key=myuser:ATBB123abcDEF456ghiJKL789mnoPQR
[INFO] Response received: 200 OK
`,
want: []string{"myuser:ATBB123abcDEF456ghiJKL789mnoPQR"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{}{AQAAABAAA https://trufflesec:ATBBa9iO-tyg7u_op@bitbucket.org}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"trufflesec:ATBBa9iO-tyg7u_op"},
},
{
name: "valid app password by itself (should not be found)",
input: "ATBB123abcDEF456ghiJKL789mnoPQR",
want: []string{},
},
{
name: "pair with invalid username",
input: "my-very-long-username-that-is-over-thirty-characters:ATBB123abcDEF456ghiJKL789mnoPQR",
want: []string{},
},
{
name: "url pattern",
input: `https://anotheruser:ATBB123abcDEF456ghiJKL789mnoPQR@bitbucket.org`,
want: []string{"anotheruser:ATBB123abcDEF456ghiJKL789mnoPQR"},
},
{
name: "http basic auth pattern",
input: `("basicauthuser", "ATBB123abcDEF456ghiJKL789mnoPQR")`,
want: []string{"basicauthuser:ATBB123abcDEF456ghiJKL789mnoPQR"},
},
{
name: "multiple matches",
input: `user1:ATBB123abcDEF456ghiJKL789mnoPQR and then also user2:ATBBzyxwvUT987srqPON654mlkJIH`,
want: []string{"user1:ATBB123abcDEF456ghiJKL789mnoPQR", "user2:ATBBzyxwvUT987srqPON654mlkJIH"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/bitcoinaverage/bitcoinaverage.go
================================================
package bitcoinaverage
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bitcoinaverage"}) + `\b([a-zA-Z0-9]{43})\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{"bitcoinaverage"}
}
type response struct {
Msg string `json:"msg"`
Success bool `json:"success"`
}
// FromData will find and optionally verify BitcoinAverage 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_BitcoinAverage,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBitcoinAverage(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_BitcoinAverage
}
func (s Scanner) Description() string {
return "BitcoinAverage is a service that provides cryptocurrency market data. BitcoinAverage API keys can be used to access and retrieve this market data."
}
// docs: https://apiv2.bitcoinaverage.com/#authentication
func verifyBitcoinAverage(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://apiv2.bitcoinaverage.com/websocket/v3/get_ticket", nil)
if err != nil {
return false, err
}
req.Header.Add("x-ba-key", key)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
apiResponse := &response{}
if err = json.NewDecoder(resp.Body).Decode(apiResponse); err != nil {
return false, err
}
if apiResponse.Success {
return true, nil
}
return false, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/bitcoinaverage/bitcoinaverage_integration_test.go
================================================
//go:build detectors
// +build detectors
package bitcoinaverage
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBitcoinAverage_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BITCOINAVERAGE")
inactiveSecret := testSecrets.MustGetField("BITCOINAVERAGE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bitcoinaverage secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BitcoinAverage,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bitcoinaverage secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BitcoinAverage,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("BitcoinAverage.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("BitcoinAverage.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/bitcoinaverage/bitcoinaverage_test.go
================================================
package bitcoinaverage
import (
"context"
"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"
)
func TestBitCoinAverage_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
secret := os.GetEnv("BITCOINAVERAGE")
if secret == ""{
// bitcoinaverage secret
secret = "WZizqeWvRnhZmFlpc5pMc92NP1Du19wxxpd5zjsYY8F"
}
req.Header.Set("x-ba-key", secret)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"WZizqeWvRnhZmFlpc5pMc92NP1Du19wxxpd5zjsYY8F"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{bitcoinaverage}{bitcoinaverage AQAAABAAA gVXtVKIj5CO3b0F12XjibnE2TvwS5rL5nJ0kQ2NZkso}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"gVXtVKIj5CO3b0F12XjibnE2TvwS5rL5nJ0kQ2NZkso"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
secret := os.GetEnv("BITCOINAVERAGE")
if secret == ""{
// bitcoinaverage secret
secret = "DyV64pq66z15thg8fh3&cd00l35lpyg7c82$"
}
req.Header.Set("x-ba-key", secret)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/bitfinex/bitfinex.go
================================================
package bitfinex
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha512"
"encoding/hex"
"fmt"
"io"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// related resource https://medium.com/@Bitfinex/api-development-update-april-65fe52f84124
apiKeyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bitfinex"}) + `\b([A-Za-z0-9_-]{43})\b`)
apiSecretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bitfinex"}) + `\b([A-Za-z0-9_-]{43})\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{"bitfinex"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Bitfinex
}
func (s Scanner) Description() string {
return "Bitfinex is a cryptocurrency exchange offering various trading options. Bitfinex API keys can be used to access and manage trading accounts."
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Bitfinex 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)
var uniqueAPIKeys, uniqueAPISecrets = make(map[string]struct{}), make(map[string]struct{})
for _, apiKey := range apiKeyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueAPIKeys[apiKey[1]] = struct{}{}
}
for _, apiSecret := range apiSecretPat.FindAllStringSubmatch(dataStr, -1) {
uniqueAPISecrets[apiSecret[1]] = struct{}{}
}
for apiKey := range uniqueAPIKeys {
for apiSecret := range uniqueAPISecrets {
// as both patterns are same, avoid verifying same string for both
if apiKey == apiSecret {
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Bitfinex,
Raw: []byte(apiKey),
}
if verify {
isVerified, verificationErr := verifyBitfinex(ctx, s.getClient(), apiKey, apiSecret)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
}
return results, nil
}
// docs: https://docs.bitfinex.com/docs/introduction
func verifyBitfinex(ctx context.Context, client *http.Client, apiKey, apiSecret string) (bool, error) {
baseURL := "https://api.bitfinex.com"
requestPath := "/v2/auth/r/wallets"
signaturePath := "/api" + requestPath
nonce := fmt.Sprintf("%d", time.Now().UnixNano()/int64(time.Microsecond))
body := "{}"
signaturePayload := signaturePath + nonce + body
signature, err := sign(signaturePayload, apiSecret)
if err != nil {
return false, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+requestPath, bytes.NewBuffer([]byte(body)))
if err != nil {
return false, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("bfx-apikey", apiKey)
req.Header.Set("bfx-signature", signature)
req.Header.Set("bfx-nonce", nonce)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusInternalServerError:
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, err
}
if strings.Contains(string(body), "apikey: digest invalid") || strings.Contains(string(body), "apikey: invalid") {
return false, nil
} else {
return false, fmt.Errorf("failed to verify Bitfinex API key, error: %s", string(body))
}
default:
return false, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode)
}
}
func sign(msg, apiSecret string) (string, error) {
sig := hmac.New(sha512.New384, []byte(apiSecret))
_, err := sig.Write([]byte(msg))
if err != nil {
return "", nil
}
return hex.EncodeToString(sig.Sum(nil)), nil
}
================================================
FILE: pkg/detectors/bitfinex/bitfinex_integration_test.go
================================================
//go:build detectors
// +build detectors
package bitfinex
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBitfinex_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
apiKey := testSecrets.MustGetField("BITFINEX_API_KEY")
inactiveApiKey := testSecrets.MustGetField("BITFINEX_API_KEY_INACTIVE")
apiSecret := testSecrets.MustGetField("BITFINEX_API_SECRET")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bitfinex api key %s within bitfinex api secret %s", apiKey, apiSecret)),
verify: true,
},
want: []detectors.Result{
// will try to scan (apiKey, apiSecret) which will verify then (apiSecret, apiKey) which will not since both parameters have equal length
{
DetectorType: detectorspb.DetectorType_Bitfinex,
Verified: true,
},
{
DetectorType: detectorspb.DetectorType_Bitfinex,
Verified: false,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bitfinex api key %s within bitfinex api secret %s", inactiveApiKey, apiSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bitfinex,
Verified: false,
},
{
DetectorType: detectorspb.DetectorType_Bitfinex,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Bitfinex.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Bitfinex.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/bitfinex/bitfinex_test.go
================================================
package bitfinex
import (
"context"
"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"
)
func TestBitFinex_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
bitfinexKey := "HxfuG198amaeCcYkASkto5VuIO-oXcplDV6JZ7OIEQZ"
bitfinexSecret := "Pf3-3v989gPbJT54D3oDBiFZmJoLpWoTHGvF8xuSBPP"
http.DefaultClient = client
c := rest.NewClientWithURL(*api).Credentials(key, secret)
}
`,
want: []string{"HxfuG198amaeCcYkASkto5VuIO-oXcplDV6JZ7OIEQZ", "Pf3-3v989gPbJT54D3oDBiFZmJoLpWoTHGvF8xuSBPP"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{bitfinex bdiODwPukXLKUjSLvfeTlKVEwm89zqOhQ2a9chacKcr}{bitfinex AQAAABAAA MTvK78juiZmddv3eEyoz1gqRwP89OHreiX6fnXkfbce}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{
"bdiODwPukXLKUjSLvfeTlKVEwm89zqOhQ2a9chacKcr",
"MTvK78juiZmddv3eEyoz1gqRwP89OHreiX6fnXkfbce",
},
},
{
name: "invalid pattern",
input: `
func main() {
bitfinexKey := "HxfuG198amaeCcYkASkto5VuIO-oXcplDV6JZ7OIEQZ"
bitfinexSecret := "kASkto5VuIO%c^HxfuG198amaeCcYkASkto5VuIO"
http.DefaultClient = client
c := rest.NewClientWithURL(*api).Credentials(key, secret)
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/bitlyaccesstoken/bitlyaccesstoken.go
================================================
package bitlyaccesstoken
import (
"context"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bitly"}) + `\b([a-zA-Z-0-9]{40})\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{"bitly"}
}
// FromData will find and optionally verify BitLyAccessToken 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)
var uniqueTokens = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueTokens[matches[1]] = struct{}{}
}
for token := range uniqueTokens {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_BitLyAccessToken,
Raw: []byte(token),
}
if verify {
isVerified, verificationErr := verifyBitlyAccessToken(ctx, client, token)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_BitLyAccessToken
}
func (s Scanner) Description() string {
return "Bitly is a URL shortening service. Bitly access tokens can be used to interact with the Bitly API, allowing users to create, manage, and track shortened URLs."
}
func verifyBitlyAccessToken(ctx context.Context, client *http.Client, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api-ssl.bitly.com/v4/user", nil)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/bitlyaccesstoken/bitlyaccesstoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package bitlyaccesstoken
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBitLyAccessToken_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BITLYACCESSTOKEN_TOKEN")
inactiveSecret := testSecrets.MustGetField("BITLYACCESSTOKEN_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bitlyaccesstoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BitLyAccessToken,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bitlyaccesstoken secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BitLyAccessToken,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("BitLyAccessToken.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("BitLyAccessToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/bitlyaccesstoken/bitlyaccesstoken_test.go
================================================
package bitlyaccesstoken
import (
"context"
"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"
)
func TestBitlyAccessToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bitlyToken := "2xN7puShxzNf5fZleQthTg305lKr7KrbW95D3gSD"
req.Header.Set("Authorization", "Bearer " + bitlyToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"2xN7puShxzNf5fZleQthTg305lKr7KrbW95D3gSD"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{bitly}{bitly AQAAABAAA TKymDGZ62qKyWXsq00Nyp-w1bTJn7bFlXWTaH-2i}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"TKymDGZ62qKyWXsq00Nyp-w1bTJn7bFlXWTaH-2i"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bitlyToken := "2xN7puShxzNf5fZleQthTg305l95D3gSD%c^"
req.Header.Set("Authorization", "Bearer " + bitlyToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/bitmex/bitmex.go
================================================
package bitmex
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bitmex"}) + `([ \r\n]{1}[0-9a-zA-Z\-\_]{24}[ \r\n]{1})`)
secretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bitmex"}) + `([ \r\n]{1}[0-9a-zA-Z\-\_]{48}[ \r\n]{1})`)
)
// 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{"bitmex"}
}
// FromData will find and optionally verify Bitmex 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
secretMatches := secretPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, secretMatch := range secretMatches {
resSecretMatch := strings.TrimSpace(secretMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Bitmex,
Raw: []byte(resSecretMatch),
RawV2: []byte(resMatch + resSecretMatch),
}
if verify {
isVerified, verificationErr := verifyBitmex(ctx, client, resMatch, resSecretMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Bitmex
}
func (s Scanner) Description() string {
return "Bitmex is a cryptocurrency exchange and derivative trading platform. Bitmex API keys can be used to access and trade on the platform programmatically."
}
// docs: https://www.bitmex.com/app/apiKeysUsage
func verifyBitmex(ctx context.Context, client *http.Client, key, secret string) (bool, error) {
timestamp := strconv.FormatInt(time.Now().Unix()+5, 10)
action := "GET"
path := "/api/v1/user"
payload := url.Values{}
signature := getBitmexSignature(timestamp, secret, action, path, payload.Encode())
req, err := http.NewRequestWithContext(ctx, action, "https://www.bitmex.com"+path, strings.NewReader(payload.Encode()))
if err != nil {
return false, err
}
req.Header.Add("api-expires", timestamp)
req.Header.Add("api-key", key)
req.Header.Add("api-signature", signature)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
func getBitmexSignature(timeStamp string, secret string, action string, path string, payload string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(action + path + timeStamp + payload))
macsum := mac.Sum(nil)
return hex.EncodeToString(macsum)
}
================================================
FILE: pkg/detectors/bitmex/bitmex_integration_test.go
================================================
//go:build detectors
// +build detectors
package bitmex
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBitmex_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
key := testSecrets.MustGetField("BITMEX_KEY")
inactiveKey := testSecrets.MustGetField("BITMEX_KEY_INACTIVE")
secret := testSecrets.MustGetField("BITMEX")
inactiveSecret := testSecrets.MustGetField("BITMEX_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bitmex key %s with bitmex secret %s within", key, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bitmex,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bitmex key %s with bitmex secret %s within but not valid", inactiveKey, inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bitmex,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Bitmex.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])
}
got[i].Raw = nil
if len(got[i].RawV2) == 0 {
t.Fatalf("no raw v2 secret present: \n %+v", got[i])
}
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Bitmex.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/bitmex/bitmex_test.go
================================================
package bitmex
import (
"context"
"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"
)
func TestBitmex_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bitmexKey := " EPwUIxOIveS463D_2O9LFgkz "
bitmexSecret := " W_HlMtrmELzXm4bSlWv49JLcgvg5hvu467WbbnpmoEA-RjrY "
signature, err := generateSecretSignature(bitmexKey, bitmexSecret)
if err != nil{
return err
}
req.Header.Set("api-signature", signature)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"EPwUIxOIveS463D_2O9LFgkzW_HlMtrmELzXm4bSlWv49JLcgvg5hvu467WbbnpmoEA-RjrY"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{bitmex EPwUIxOIveS463D_2O9LFgkz }{bitmex AQAAABAAA W_HlMtrmELzXm4bSlWv49JLcgvg5hvu467WbbnpmoEA-RjrY }configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"EPwUIxOIveS463D_2O9LFgkzW_HlMtrmELzXm4bSlWv49JLcgvg5hvu467WbbnpmoEA-RjrY"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bitmexKey := "mELzXm4bSlWv49JLc%c^"
bitmexSecret := "IXpH-fJJiLFn--Wo7rnlXE"
signature, err := generateSecretSignature(bitmexKey, bitmexSecret)
if err != nil{
return err
}
req.Header.Set("api-signature", signature)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/blazemeter/blazemeter.go
================================================
package blazemeter
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"blazemeter", "runscope"}) + `\b([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})\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{"blazemeter", "runscope"}
}
// FromData will find and optionally verify Blazemeter 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Blazemeter,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBlazeMeter(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Blazemeter
}
func (s Scanner) Description() string {
return "Blazemeter is a continuous testing platform for DevOps. The keys can be used to access and manage performance tests and other resources."
}
// docs: https://help.blazemeter.com/apidocs/api-monitoring/account.htm?tocpath=API%20Monitoring%7C_____12
func verifyBlazeMeter(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.runscope.com/account", nil)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/blazemeter/blazemeter_integration_test.go
================================================
//go:build detectors
// +build detectors
package blazemeter
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBlazemeter_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BLAZEMETER")
inactiveSecret := testSecrets.MustGetField("BLAZEMETER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a blazemeter secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Blazemeter,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a blazemeter secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Blazemeter,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Blazemeter.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Blazemeter.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/blazemeter/blazemeter_test.go
================================================
package blazemeter
import (
"context"
"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"
)
func TestBlazeMeter_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
blazemeterToken := "sjbuxa3m-vs4n-ykl8-8jpv-i09hdidciolp"
req.Header.Set("Authorization", "Bearer " + blazemeterToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"sjbuxa3m-vs4n-ykl8-8jpv-i09hdidciolp"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{runscope}{runscope AQAAABAAA vzn9dy84-mnvd-alqd-4pbf-cn618kvo26le}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"vzn9dy84-mnvd-alqd-4pbf-cn618kvo26le"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
blazemeterToken := "sjbuxa3m-vs4n- ykl8-8jpv#i09hdidciolp"
req.Header.Set("Authorization", "Bearer " + blazemeterToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/blitapp/blitapp.go
================================================
package blitapp
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"blitapp"}) + `\b([a-zA-Z0-9_-]{39})\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{"blitapp"}
}
// FromData will find and optionally verify BlitApp 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_BlitApp,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBlitApp(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_BlitApp
}
func (s Scanner) Description() string {
return "BlitApp is a service used for managing applications. BlitApp API keys can be used to access and modify application data."
}
// docs: https://blitapp.com/api/#/App/get_apps_all
func verifyBlitApp(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://blitapp.com/api/apps/all", nil)
if err != nil {
return false, nil
}
req.Header.Add("API-Key", key)
resp, err := client.Do(req)
if err != nil {
return false, nil
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/blitapp/blitapp_integration_test.go
================================================
//go:build detectors
// +build detectors
package blitapp
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBlitApp_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BLITAPP")
inactiveSecret := testSecrets.MustGetField("BLITAPP_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a blitapp secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BlitApp,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a blitapp secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BlitApp,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("BlitApp.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("BlitApp.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/blitapp/blitapp_test.go
================================================
package blitapp
import (
"context"
"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"
)
func TestBlitApp_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
blitAppKey := "I_MncTA8nlFcqlBCakI5vwkwFD4_zRUYZKt8hyd"
req.Header.Set("API-Key", blitAppKey)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"I_MncTA8nlFcqlBCakI5vwkwFD4_zRUYZKt8hyd"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{blitapp}{blitapp AQAAABAAA 188hN_78_V86WbCBVJd6OLMQJTHva7cbSf8HDFo}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"188hN_78_V86WbCBVJd6OLMQJTHva7cbSf8HDFo"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
blitAppKey := "I_Mn%^&*qlBCakI5vwkwFD4_zRUY"
req.Header.Set("API-Key", blitAppKey)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/blocknative/blocknative.go
================================================
package blocknative
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"blocknative"}) + `\b([0-9Aa-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\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{"blocknative"}
}
// FromData will find and optionally verify Blocknative 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_BlockNative,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBlocknative(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_BlockNative
}
func (s Scanner) Description() string {
return "Blocknative is a platform that provides real-time blockchain transaction monitoring and notification services. Blocknative API keys can be used to access and interact with these services."
}
// docs: https://docs.blocknative.com/gas-prediction/gas-platform#api-endpoint
func verifyBlocknative(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.blocknative.com/gasprices/blockprices", nil)
if err != nil {
return false, err
}
req.Header.Add("Authorization", key)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
// Right now the blocknative API logic is broken and return 200 for invalid key as well
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized, http.StatusTooManyRequests:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/blocknative/blocknative_integration_test.go
================================================
//go:build detectors
// +build detectors
package blocknative
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBlocknative_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BLOCKNATIVE")
inactiveSecret := testSecrets.MustGetField("BLOCKNATIVE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a blocknative secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BlockNative,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a blocknative secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BlockNative,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Blocknative.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Blocknative.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/blocknative/blocknative_test.go
================================================
package blocknative
import (
"context"
"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"
)
func TestBlockNative_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
blocknativeSecret := "76e50995-059f-3d1a-af8e-cc85fc05eb03"
req.Header.Set("Authorization", blocknativeSecret)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"76e50995-059f-3d1a-af8e-cc85fc05eb03"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{blocknative}{blocknative AQAAABAAA 7b15f7f8-52a8-849d-384e-20b4c0de82dd}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"7b15f7f8-52a8-849d-384e-20b4c0de82dd"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
blocknativeSecret := "2xN7puShxzNf5fZleQthTg305l95D3gSD%c^"
req.Header.Set("Authorization", blocknativeSecret)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/blogger/blogger.go
================================================
package blogger
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"blogger"}) + `\b([0-9A-Za-z-]{39})\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{"blogger"}
}
// FromData will find and optionally verify Blogger 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Blogger,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBlogger(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Blogger
}
func (s Scanner) Description() string {
return "Blogger API keys can be used to access and manage blogs on the Blogger platform."
}
// docs: https://developers.google.com/blogger/docs/3.0/using
func verifyBlogger(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.googleapis.com/blogger/v3/blogs/2399953?key="+key, nil)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusBadRequest, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/blogger/blogger_integration_test.go
================================================
//go:build detectors
// +build detectors
package blogger
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBlogger_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BLOGGER")
inactiveSecret := testSecrets.MustGetField("BLOGGER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a blogger secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Blogger,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a blogger secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Blogger,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Blogger.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Blogger.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/blogger/blogger_test.go
================================================
package blogger
import (
"context"
"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"
)
func TestBlogger_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", "https://api.example.com/v1/blogger/blogs?key=fnWLw7pz1tc6uCzq6qocQZIxRF6SqUaOOkLqePY", http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
// Check response status
if resp.StatusCode == http.StatusOK {
fmt.Println("Request successful!")
} else {
fmt.Println("Request failed with status:", resp.Status)
}
}
`,
want: []string{"fnWLw7pz1tc6uCzq6qocQZIxRF6SqUaOOkLqePY"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{blogger}{blogger AQAAABAAA mtkwpygpNROxOgLZCnEvl7gNme1IuFiQm9oxPzJ}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"mtkwpygpNROxOgLZCnEvl7gNme1IuFiQm9oxPzJ"},
},
{
name: "invalid pattern",
input: `
func main() {
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", "https://api.example.com/v1/blogger/blogs?key=fnWL(w7pz1t)6uCz-q6qocQZIxRF6S/UqePY", http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
// Check response status
if resp.StatusCode == http.StatusOK {
fmt.Println("Request successful!")
} else {
fmt.Println("Request failed with status:", resp.Status)
}
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/bombbomb/bombbomb.go
================================================
package bombbomb
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bombbomb"}) + common.BuildRegexJWT("0,140", "0,419", "0,171"))
)
// 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{"bombbomb"}
}
// FromData will find and optionally verify BombBomb 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_BombBomb,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBombBomb(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_BombBomb
}
func (s Scanner) Description() string {
return "BombBomb is a video messaging platform that allows users to create and send video emails. BombBomb API keys can be used to access and manage video email campaigns and contacts."
}
// docs: https://developer.bombbomb.com/api#operations-Users-UserInfo
func verifyBombBomb(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.bombbomb.com/v2/user/", nil)
if err != nil {
return false, err
}
req.Header.Add("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/bombbomb/bombbomb_integration_test.go
================================================
//go:build detectors
// +build detectors
package bombbomb
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBombBomb_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BOMBBOMB")
inactiveSecret := testSecrets.MustGetField("BOMBBOMB_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bombbomb secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BombBomb,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bombbomb secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BombBomb,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("BombBomb.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("BombBomb.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/bombbomb/bombbomb_test.go
================================================
package bombbomb
import (
"context"
"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"
)
func TestBombBomb_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
bombbombToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
req.Header.Set("Authorization", bombbombToken)
`,
want: []string{"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{bombbomb}{bombbomb AQAAABAAA eyJioGciOiJIU9I1NiIsInR5cCI6IkpXVCJ9.eyJJdWIiOiIxMjM0NTY3ODkwIiwibmFtZSJ6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5d}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"eyJioGciOiJIU9I1NiIsInR5cCI6IkpXVCJ9.eyJJdWIiOiIxMjM0NTY3ODkwIiwibmFtZSJ6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5d"},
},
{
name: "invalid pattern",
input: `
bombbombToken := "eyJhbGciOiJIUzI1N^iIsInRkpXVCJ9.ey$JzdWIiOiIxMjM0NTY3ODkwIiwibmFtZwiaWF0IjoxNTE2MjM5MDIyfQ.S&flKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
req.Header.Set("Authorization", bombbombToken)
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/boostnote/boostnote.go
================================================
package boostnote
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"boostnote"}) + `\b([0-9a-f]{64})\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{"boostnote"}
}
// FromData will find and optionally verify BoostNote 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_BoostNote,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBoostnote(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_BoostNote
}
func (s Scanner) Description() string {
return "BoostNote is a note-taking application. The secret detected here is likely an API key or token used to access BoostNote services."
}
// docs: https://boostnote.io/features/public-api
func verifyBoostnote(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://boostnote.io/api/docs", nil)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/boostnote/boostnote_integration_test.go
================================================
//go:build detectors
// +build detectors
package boostnote
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBoostNote_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BOOSTNOTE")
inactiveSecret := testSecrets.MustGetField("BOOSTNOTE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a boostnote secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BoostNote,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a boostnote secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BoostNote,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("BoostNote.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("BoostNote.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/boostnote/boostnote_test.go
================================================
package boostnote
import (
"context"
"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"
)
func TestBoostNote_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
boostnoteKey := "fb1026ac5994e3ad01799fe040289317ba2594a20e9e45307a143be82b49d213"
req.Header.Set("Authorization", "Bearer " + boostnoteKey)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"fb1026ac5994e3ad01799fe040289317ba2594a20e9e45307a143be82b49d213"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{boostnote}{boostnote AQAAABAAA a546e80a8018e1c5e37e4a3366a20aa363489691d2ca335e3a082550d8a92120}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"a546e80a8018e1c5e37e4a3366a20aa363489691d2ca335e3a082550d8a92120"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
boostnoteKey := "#^fb1026ac59=4e3ad01799fe04028931___4a20e9e45307a143be82b49d213$"
req.Header.Set("Authorization", "Bearer " + boostnoteKey)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/borgbase/borgbase.go
================================================
package borgbase
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"borgbase"}) + `\b([a-zA-Z0-9/_.-]{148,152})\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{"borgbase"}
}
// FromData will find and optionally verify Borgbase 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Borgbase,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBorgbase(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Borgbase
}
func (s Scanner) Description() string {
return "Borgbase is a service for hosting Borg repositories. Borgbase API keys can be used to manage and access these repositories."
}
// docs: https://docs.borgbase.com/api
func verifyBorgbase(ctx context.Context, client *http.Client, key string) (bool, error) {
timeout := 10 * time.Second
client.Timeout = timeout
payload := strings.NewReader(`{"query":"{ sshList {id, name}}"}`)
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.borgbase.com/graphql", payload)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return false, err
}
bodyString := string(bodyBytes)
validResponse := strings.Contains(bodyString, `"sshList":[]`)
if validResponse {
return true, nil
}
return false, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/borgbase/borgbase_integration_test.go
================================================
//go:build detectors
// +build detectors
package borgbase
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBorgbase_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BORGBASE")
inactiveSecret := testSecrets.MustGetField("BORGBASE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a borgbase secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Borgbase,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a borgbase secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Borgbase,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Borgbase.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Borgbase.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/borgbase/borgbase_test.go
================================================
package borgbase
import (
"context"
"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"
)
func TestBorgBase_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
payload := '{"query":"{ sshList {id, name}}"}'
req, err := http.NewRequest("POST", url, payload)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
borgbaseToken := "FoHclCFSi_aV09jowJQ4RUF_MiqW6ioqq6_OcyB0PFlV-mQ1yoFjk5JLlxbzRUzKTA6vsfR8wq6TNc83rtNKlkD092Sj1c9CbPVBXlHksy.sT2I/so6bMGdPcqxzbjrxYgAUiORgqJDeTet4gKOQlZpt"
req.Header.Set("Authorization", "Bearer " + borgbaseToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"FoHclCFSi_aV09jowJQ4RUF_MiqW6ioqq6_OcyB0PFlV-mQ1yoFjk5JLlxbzRUzKTA6vsfR8wq6TNc83rtNKlkD092Sj1c9CbPVBXlHksy.sT2I/so6bMGdPcqxzbjrxYgAUiORgqJDeTet4gKOQlZpt"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{borgbase}{borgbase AQAAABAAA KtSE0ggsVsvvDQPHau2ItXW8yi7YsFTho4wHTTjCDShrWgYA421GzfXMwkOYklS6psQd1W8459NvmcZSmr7_LKqQffBGYAVvexM1D4JxRcQS49H3rnFlwDYspB5_m7AxvmbPrpWj8TfNm7zKCa2Ed}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"KtSE0ggsVsvvDQPHau2ItXW8yi7YsFTho4wHTTjCDShrWgYA421GzfXMwkOYklS6psQd1W8459NvmcZSmr7_LKqQffBGYAVvexM1D4JxRcQS49H3rnFlwDYspB5_m7AxvmbPrpWj8TfNm7zKCa2Ed"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
payload := '{"query":"{ sshList {id, name}}"}'
req, err := http.NewRequest("POST", url, payload)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
borgbaseToken := "mQ1yoFjk5JLlxbzRUzKTA6vsfR8wq,6TNc83rtNKlkD092Sj1c9CbPVBXlHksy%c^so6bMGdPcqxzbjrxYgAUiORgqJDeTet4gKOQlZpt"
req.Header.Set("Authorization", "Bearer " + borgbaseToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/box/box.go
================================================
package box
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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"box"}) + `\b([0-9a-zA-Z]{32})\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{"box"}
}
func (s Scanner) Description() string {
return "Box is a service offering various service for secure collaboration, content management, and workflow. Box token can be used to access and interact with this data."
}
// FromData will find and optionally verify Box 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 match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Box,
Raw: []byte(match),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, extraData, verificationErr := verifyMatch(ctx, client, match)
s1.Verified = isVerified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr, match)
}
results = append(results, s1)
}
return
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, map[string]string, error) {
url := "https://api.box.com/2.0/users/me"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return false, nil, err
}
req.Header = http.Header{"Authorization": []string{"Bearer " + token}}
req.Header.Add("content-type", "application/json")
res, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
{
var u user
if err := json.NewDecoder(res.Body).Decode(&u); err != nil {
return false, nil, err
}
return true, bakeExtraDataFromUser(u), nil
}
case http.StatusUnauthorized:
// 401 access token not found
// The secret is determinately not verified (nothing to do)
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Box
}
func bakeExtraDataFromUser(u user) map[string]string {
return map[string]string{
"user_id": u.ID,
"username": u.Login,
"user_status": u.Status,
}
}
// struct to represent a Box user.
type user struct {
ID string `json:"id"`
Login string `json:"login"`
Status string `json:"status"`
}
================================================
FILE: pkg/detectors/box/box_integration_test.go
================================================
//go:build detectors
// +build detectors
package box
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/detectorspb"
)
func TestBox_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)
}
token := testSecrets.MustGetField("BOX_ACCESS_TOKEN")
inactiveToken := testSecrets.MustGetField("BOX_ACCESS_TOKEN_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a box token %s within", token)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Box,
Verified: true,
Raw: []byte(token),
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a box token %s within but not valid", inactiveToken)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Box,
Verified: false,
Raw: []byte(inactiveToken),
},
},
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 box token %s within", token)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Box,
Verified: false,
Raw: []byte(token),
},
},
wantErr: false,
wantVerificationErr: true,
},
{
name: "found, verified but unexpected api surface",
s: Scanner{client: common.ConstantResponseHttpClient(500, "")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a box secret %s within", token)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Box,
Verified: false,
Raw: []byte(token),
},
},
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("Box.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "verificationError", "ExtraData")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Box.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/box/box_test.go
================================================
package box
import (
"context"
"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"
)
func TestBox_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] request received to fetch box data
[INFO] sending API request to box API
[DEBUG] using Key=Ogowv5cj5AJJjO5F3daNHbKJDdPud0CZ
[DEBUG] request sent successfully
[INFO] response received: 200 OK
[DEBUG] fetch data from the database for ID Qje1HjJmgrNzOQpQZROEeYjmHbD2qdFF
[INFO] data returned
`,
want: []string{"Ogowv5cj5AJJjO5F3daNHbKJDdPud0CZ"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{box}{box AQAAABAAA Dxb2zNdFF2QTSMwrZJnoeD54Dc4zZAIW}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"Dxb2zNdFF2QTSMwrZJnoeD54Dc4zZAIW"},
},
{
name: "invalid pattern",
input: `
[INFO] request received to fetch box data
[INFO] sending API request to box API
[DEBUG] using Key=Ogow-v5cj-5AJJ-jO5F-3daN-HbKJ-DdPu-d0CZ
[DEBUG] request sent successfully
[ERROR] response received: 401 UnAuthorized
[INFO] nothing to return
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/boxoauth/boxoauth.go
================================================
package boxoauth
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
clientIdPat = regexp.MustCompile(detectors.PrefixRegex([]string{"id"}) + `\b([a-zA-Z0-9]{32})\b`)
clientSecretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"secret"}) + `\b([a-zA-Z0-9]{32})\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{"box"}
}
func (s Scanner) Description() string {
return "Box is a service offering various service for secure collaboration, content management, and workflow. Box Oauth credentials can be used to access and interact with this data."
}
// FromData will find and optionally verify Box 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)
uniqueIdMatches := make(map[string]struct{})
for _, match := range clientIdPat.FindAllStringSubmatch(dataStr, -1) {
uniqueIdMatches[match[1]] = struct{}{}
}
uniqueSecretMatches := make(map[string]struct{})
for _, match := range clientSecretPat.FindAllStringSubmatch(dataStr, -1) {
uniqueSecretMatches[match[1]] = struct{}{}
}
for resIdMatch := range uniqueIdMatches {
for resSecretMatch := range uniqueSecretMatches {
// ignore if the id and secret are the same
if resIdMatch == resSecretMatch {
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_BoxOauth,
Raw: []byte(resIdMatch),
RawV2: []byte(resIdMatch + resSecretMatch),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, extraData, verificationErr := verifyMatch(ctx, client, resIdMatch, resSecretMatch)
s1.Verified = isVerified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr, resIdMatch)
}
results = append(results, s1)
// box client supports only one client id and secret pair
if s1.Verified {
break
}
}
}
return
}
func verifyMatch(ctx context.Context, client *http.Client, id string, secret string) (bool, map[string]string, error) {
url := "https://api.box.com/oauth2/token"
payload := strings.NewReader("grant_type=client_credentials&client_id=" + id + "&client_secret=" + secret)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, payload)
if err != nil {
return false, nil, err
}
req.Header = http.Header{"content-type": []string{"application/x-www-form-urlencoded"}}
res, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
// We are using malformed request to check if the client id and secret are valid.
// In this case, the Box OAuth API returns a 400 status code even if the credentials are valid.
//
// - If the client ID/secret are valid, the response contains "unauthorized_client"
// - If the credentials are invalid, the response contains "invalid_client"
//
// So we check the response body for one of these keywords.
switch res.StatusCode {
case http.StatusBadRequest:
{
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, nil, err
}
body := string(bodyBytes)
if strings.Contains(body, "unauthorized_client") {
return true, nil, nil
} else if strings.Contains(body, "invalid_client") {
return false, nil, nil
} else {
return false, nil, fmt.Errorf("response body missing expected keyword")
}
}
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_BoxOauth
}
================================================
FILE: pkg/detectors/boxoauth/boxoauth_integration_test.go
================================================
//go:build detectors
// +build detectors
package boxoauth
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBoxOauth_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)
}
id := testSecrets.MustGetField("BOXOAUTH_ID")
secret := testSecrets.MustGetField("BOXOAUTH_SECRET")
invalidSecret := testSecrets.MustGetField("BOXOAUTH_INVALID_SECRET")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a box id %s with secret %s", id, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BoxOauth,
Verified: true,
Raw: []byte(id),
RawV2: []byte(id + secret),
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a box id %s with secret %s", id, invalidSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BoxOauth,
Verified: false,
Raw: []byte(id),
RawV2: []byte(id + invalidSecret),
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("BoxOauth.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 len(got[i].Raw) == 0 {
t.Fatalf("no rawV2 secret present: \n %+v", got[i])
}
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("BoxOauth.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/boxoauth/boxoauth_test.go
================================================
package boxoauth
import (
"context"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
clientId = common.GenerateRandomPassword(true, true, true, false, 32)
clientSecret = common.GenerateRandomPassword(true, true, true, false, 32)
invalidClientSecret = common.GenerateRandomPassword(true, true, true, true, 32)
)
func TestBoxOauth_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: fmt.Sprintf("box id = '%s' box secret = '%s'", clientId, clientSecret),
want: []string{clientId + clientSecret},
},
{
name: "invalid pattern",
input: fmt.Sprintf("box id = '%s' box secret = '%s'", clientId, invalidClientSecret),
want: nil,
},
{
name: "invalid pattern",
input: fmt.Sprintf("box = '%s|%s'", clientId, invalidClientSecret),
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/braintreepayments/braintreepayments.go
================================================
package braintreepayments
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
client *http.Client
useTestURL bool
}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
const (
verifyURL = "https://payments.braintree-api.com/graphql"
verifyTestURL = "https://payments.sandbox.braintree-api.com/graphql"
)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"braintree"}) + `\b([0-9a-f]{32})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"braintree"}) + `\b([0-9a-z]{16})\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{"braintree"}
}
// FromData will find and optionally verify BraintreePayments 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
resIdMatch := strings.TrimSpace(idMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_BraintreePayments,
Raw: []byte(resMatch),
}
if verify {
client := s.getClient()
url := s.getBraintreeURL()
isVerified, verificationErr := verifyBraintree(ctx, client, url, resIdMatch, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) getBraintreeURL() string {
if s.useTestURL {
return verifyTestURL
}
return verifyURL
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
func verifyBraintree(ctx context.Context, client *http.Client, url, pubKey, privKey string) (bool, error) {
payload := strings.NewReader(`{"query": "query { ping }"}`)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, payload)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Braintree-Version", "2019-01-01")
req.SetBasicAuth(pubKey, privKey)
res, err := client.Do(req)
if err != nil {
return false, err
}
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
bodyString := string(bodyBytes)
if !(res.StatusCode == http.StatusOK) {
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
validResponse := `"data":{`
if strings.Contains(bodyString, validResponse) {
return true, nil
}
return false, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_BraintreePayments
}
func (s Scanner) Description() string {
return "Braintree is a full-stack payment platform that makes it easy to accept payments in your mobile app or website. Braintree API keys can be used to access and manage payment transactions, customer data, and other payment-related operations."
}
================================================
FILE: pkg/detectors/braintreepayments/braintreepayments_integration_test.go
================================================
//go:build detectors
// +build detectors
package braintreepayments
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/detectorspb"
)
func TestBraintreePayments_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BRAINTREEPAYMENTS")
id := testSecrets.MustGetField("BRAINTREEPAYMENTS_USER")
inactiveSecret := testSecrets.MustGetField("BRAINTREEPAYMENTS_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",
s: Scanner{useTestURL: true},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a braintree secret %s within braintree %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BraintreePayments,
Verified: true,
},
},
wantErr: false,
},
{
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 braintree secret %s within braintree %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BraintreePayments,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: true,
},
{
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 braintree secret %s within braintree %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BraintreePayments,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: true,
},
{
name: "found, unverified",
s: Scanner{useTestURL: true},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a braintree secret %s within braintree %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BraintreePayments,
Verified: false,
},
},
wantErr: 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,
},
}
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("BraintreePayments.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("BraintreePayments.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/braintreepayments/braintreepayments_test.go
================================================
package braintreepayments
import (
"context"
"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"
)
func TestBrainTreePayments_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
braintreeKey := "f7b3cb83a7fdb915a71ce17ab8a903cc"
braintreeId := "kmajpm4h1pqoqxyo"
req.SetBasicAuth(braintreeKey, braintreeId)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"f7b3cb83a7fdb915a71ce17ab8a903cc"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{braintree jvbs4thxyzhh8n00}{braintree AQAAABAAA 7d1ab9c76bea2cfb80a29fef8f1e0b12}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"7d1ab9c76bea2cfb80a29fef8f1e0b12"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
braintreeKey := "f7b3cb83a7fdb915a71ce17ab8a903cckmajpm4h1pqoqxyo"
braintreeId := "kmajpm4h1pqoqxyo"
req.SetBasicAuth(braintreeKey, braintreeId)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/brandfetch/v1/brandfetch.go
================================================
package brandfetch
import (
"context"
"net/http"
"strconv"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
v2 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/brandfetch/v2"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
}
func (s Scanner) Version() int { return 1 }
var (
// Ensure the Scanner satisfies the interface at compile time.
_ detectors.Detector = (*Scanner)(nil)
_ detectors.Versioner = (*Scanner)(nil)
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"brandfetch"}) + `\b([0-9A-Za-z]{40})\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{"brandfetch"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Brandfetch
}
func (s Scanner) Description() string {
return "Brandfetch is a service that provides brand data, including logos, colors, fonts, and more. Brandfetch API keys can be used to access this data."
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Brandfetch 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)
uniqueTokenMatches := make(map[string]struct{})
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueTokenMatches[match[1]] = struct{}{}
}
for match := range uniqueTokenMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Brandfetch,
Raw: []byte(match),
ExtraData: map[string]string{"version": strconv.Itoa(s.Version())},
}
if verify {
isVerified, verificationErr := v2.VerifyMatch(ctx, s.getClient(), match)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, match)
}
results = append(results, s1)
}
return
}
================================================
FILE: pkg/detectors/brandfetch/v1/brandfetch_integration_test.go
================================================
//go:build detectors
// +build detectors
package brandfetch
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBrandfetch_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BRANDFETCH")
inactiveSecret := testSecrets.MustGetField("BRANDFETCH_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a brandfetch secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Brandfetch,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a brandfetch secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Brandfetch,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Brandfetch.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])
}
got[i].Raw = nil
got[i].ExtraData = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Brandfetch.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/brandfetch/v1/brandfetch_test.go
================================================
package brandfetch
import (
"context"
"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"
)
func TestBrandFetch_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
brandfetchAPIKey := "uHOAdwfQ7sD2yOpur72UqyUeIqnFwILOIlEPyBtJ"
req.Header.Set("x-api-key", brandfetchAPIKey) // brandfetch secret
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"uHOAdwfQ7sD2yOpur72UqyUeIqnFwILOIlEPyBtJ"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{uSiXZ-NMpDW-ZJQFSN-5wkT7SqQ8-mDbr9K2pl}{brandfetch AQAAABAAA uSiXZNMpDWWhZJQFSNkE5wkT7SqQ8B3mDbr9K2pl}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"uSiXZNMpDWWhZJQFSNkE5wkT7SqQ8B3mDbr9K2pl"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
brandfetchAPIKey := "yUeIqnFwILOIlEPyBt+=JOAdwfQ7sD2uHOAdwf2U[qy]UeIqnFwILOIlEPyBtJ^"
req.Header.Set("x-api-key", brandfetchAPIKey) // brandfetch secret
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/brandfetch/v2/brandfetch.go
================================================
package brandfetch
import (
"context"
"fmt"
"net/http"
"strconv"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
func (s Scanner) Version() int { return 2 }
var (
// Ensure the Scanner satisfies the interface at compile time.
_ detectors.Detector = (*Scanner)(nil)
_ detectors.Versioner = (*Scanner)(nil)
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"brandfetch"}) + `([a-zA-Z0-9=+/\-_!@#$%^&*()]{43}=)`)
)
// 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{"brandfetch"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Brandfetch
}
func (s Scanner) Description() string {
return "Brandfetch is a service that provides brand data, including logos, colors, fonts, and more. Brandfetch API keys can be used to access this data."
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Brandfetch 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[strings.TrimSpace(match[1])] = struct{}{}
}
for match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Brandfetch,
Raw: []byte(match),
ExtraData: map[string]string{"version": strconv.Itoa(s.Version())},
}
if verify {
isVerified, verificationErr := VerifyMatch(ctx, s.getClient(), match)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, match)
}
results = append(results, s1)
}
return
}
// verifyMatch checks if the provided Brandfetch token is valid by making a request to the Brandfetch API.
// https://docs.brandfetch.com/docs/getting-started
func VerifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.brandfetch.io/v2/brands/google.com", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+token)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/brandfetch/v2/brandfetch_integration_test.go
================================================
//go:build detectors
// +build detectors
package brandfetch
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBrandfetch_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BRANDFETCH_V2")
inactiveSecret := testSecrets.MustGetField("BRANDFETCH_V2_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a brandfetch secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Brandfetch,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a brandfetch secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Brandfetch,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Brandfetch.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])
}
got[i].Raw = nil
got[i].ExtraData = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Brandfetch.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/brandfetch/v2/brandfetch_test.go
================================================
package brandfetch
import (
"context"
"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"
)
func TestBrandFetch_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: "brandfetch credentials: ZUfake+eKo3qNxLDfake/6vqjOtr4fa6u5wShfakes8=",
want: []string{"ZUfake+eKo3qNxLDfake/6vqjOtr4fa6u5wShfakes8="},
},
{
name: "valid pattern - assignment format",
input: "BRANDFETCH_API_KEY=msCwufakeod43s2ad/D0em/LbIBpZqFAKE9P+H3UTno=",
want: []string{"msCwufakeod43s2ad/D0em/LbIBpZqFAKE9P+H3UTno="},
},
{
name: "valid pattern - complex",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
brandfetchAPIKey := "0mWrufake4X1dRfake0mxS+E48ofakesTlyl55raNOs="
req.Header.Set("x-api-key", brandfetchAPIKey) // brandfetch secret
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
// Check response status
if resp.StatusCode == http.StatusOK {
fmt.Println("Request successful!")
} else {
fmt.Println("Request failed with status:", resp.Status)
}
}
`,
want: []string{"0mWrufake4X1dRfake0mxS+E48ofakesTlyl55raNOs="},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{uSiXZ-NMpDW-ZJQFSN-5wkT7SqQ8-mDbr9K2pl}{brandfetch AQAAABAAA 0mWrufake4X1dRfake0mxS+E48ofakesTlyl55rfake=}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"0mWrufake4X1dRfake0mxS+E48ofakesTlyl55rfake="},
},
{
name: "invalid pattern - wrong length",
input: "brandfetch credentials: yUeIqnFwILOIlEPyBt+=JOAdwfQ7sD2uHOAdwf2U",
want: nil,
},
{
name: "invalid pattern - invalid characters",
input: "brandfetch credentials: yUeIqnFwILOIlEPyBt+=JOAdwfQ7sD2uHOAdwf2U[qy]UeIqnFwILOIlEPyBtJ^fakes=",
want: nil,
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
brandfetchAPIKey := "yUeIqnFwILOIlEPyBt+=JOAdwfQ7sD2uHOAdwf2U[qy]UeIqnFwILOIlEPyBtJ^"
req.Header.Set("x-api-key", brandfetchAPIKey) // brandfetch secret
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/browserstack/browserstack.go
================================================
package browserstack
import (
"context"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"strings"
regexp "github.com/wasilibs/go-re2"
"golang.org/x/net/publicsuffix"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
const browserStackAPIURL = "https://www.browserstack.com/automate/plan.json"
var (
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"hub-cloud.browserstack.com", "accessKey", "\"access_Key\":", "ACCESS_KEY", "key", "browserstackKey", "BS_AUTHKEY", "BROWSERSTACK_ACCESS_KEY"}) + `\b([0-9a-zA-Z]{20})\b`)
userPat = regexp.MustCompile(detectors.PrefixRegex([]string{"hub-cloud.browserstack.com", "userName", "\"username\":", "USER_NAME", "user", "browserstackUser", "BS_USERNAME", "BROWSERSTACK_USERNAME"}) + `\b([a-zA-Z\d]{3,18}[._-]*[a-zA-Z\d]{6,11})\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{"browserstack"}
}
func (s Scanner) getClient(cookieJar *cookiejar.Jar) *http.Client {
if s.client != nil {
s.client.Jar = cookieJar
return s.client
}
// Using custom HTTP client instead of common.SaneHttpClient() here because, for unknown reasons, browserstack blocks those requests even with cookie jar attached
return &http.Client{
Jar: cookieJar,
}
}
// FromData will find and optionally verify BrowserStack 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
userMatches := userPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, userMatch := range userMatches {
resUserMatch := strings.TrimSpace(userMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_BrowserStack,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resUserMatch),
}
if verify {
// browserstack (via cloudflare) requires cookies to be enabled
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
return nil, err
}
client := s.getClient(jar)
isVerified, verificationErr := verifyBrowserStackCredentials(ctx, client, resUserMatch, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyBrowserStackCredentials(ctx context.Context, client *http.Client, username, accessKey string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, browserStackAPIURL, nil)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("User-Agent", common.UserAgent())
req.SetBasicAuth(username, accessKey)
res, err := client.Do(req)
if err != nil {
return false, err
}
defer res.Body.Close()
if res.StatusCode == http.StatusOK {
return true, nil
} else if res.StatusCode == http.StatusForbidden {
// Sometimes browserstack (via Cloudflare) will block requests for security
body, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
if strings.Contains(string(body), "blocked") {
return false, fmt.Errorf("blocked by browserstack")
}
} else if res.StatusCode != http.StatusUnauthorized {
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
return false, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_BrowserStack
}
func (s Scanner) Description() string {
return "BrowserStack is a cloud web and mobile testing platform. BrowserStack credentials can be used to access and manage testing environments."
}
================================================
FILE: pkg/detectors/browserstack/browserstack_integration_test.go
================================================
//go:build detectors
// +build detectors
package browserstack
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/detectorspb"
)
func TestBrowserStack_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secretUser := testSecrets.MustGetField("BROWSERSTACK_USER")
secret := testSecrets.MustGetField("BROWSERSTACK")
inactiveSecret := testSecrets.MustGetField("BROWSERSTACK_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a BROWSERSTACK_ACCESS_KEY %s within BROWSERSTACK_USERNAME %s", secret, secretUser)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BrowserStack,
Verified: true,
RawV2: []byte(fmt.Sprintf("%s%s", secret, secretUser)),
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a BROWSERSTACK_ACCESS_KEY %s within BROWSERSTACK_USERNAME %s but not valid", inactiveSecret, secretUser)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BrowserStack,
Verified: false,
RawV2: []byte(fmt.Sprintf("%s%s", inactiveSecret, secretUser)),
},
},
wantErr: 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 BROWSERSTACK_ACCESS_KEY %s within BROWSERSTACK_USERNAME %s", secret, secretUser)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_BrowserStack,
RawV2: []byte(fmt.Sprintf("%s%s", secret, secretUser)),
Verified: false,
}
r.SetVerificationError(fmt.Errorf("context deadline exceeded"), secret)
results := []detectors.Result{r}
return results
}(),
wantErr: false,
},
{
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 BROWSERSTACK_ACCESS_KEY %s within BROWSERSTACK_USERNAME %s", secret, secretUser)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_BrowserStack,
Verified: false,
RawV2: []byte(fmt.Sprintf("%s%s", secret, secretUser)),
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 404"), secret)
results := []detectors.Result{r}
return results
}(),
wantErr: false,
},
{
name: "found, verified but blocked by browserstack",
s: Scanner{client: common.ConstantResponseHttpClient(403, "blocked")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a BROWSERSTACK_ACCESS_KEY %s within BROWSERSTACK_USERNAME %s", secret, secretUser)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_BrowserStack,
Verified: false,
RawV2: []byte(fmt.Sprintf("%s%s", secret, secretUser)),
}
r.SetVerificationError(fmt.Errorf("blocked by browserstack"), secret)
results := []detectors.Result{r}
return results
}(),
wantErr: 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,
},
}
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("BrowserStack.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", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("BrowserStack.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/browserstack/browserstack_test.go
================================================
package browserstack
import (
"context"
"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"
)
func TestBrowserStack_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
if browserstackKey, _ := os.GetEnv("ACCESS_KEY"); browserstackKey != "cK1bq7JREJtMf1meaGgs" {
return fmt.Errorf("invalid accessKey: %v expected: %v", browserstackKey, "1YZazUAPFOiaIFljWDhC")
}
if browserstackUser, _ := os.GetEnv("USER_NAME"); browserstackUser != "truffle-security91" {
return fmt.Errorf("invalid userName: %v", "truffle-security91")
}
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{
"cK1bq7JREJtMf1meaGgstruffle-security91",
"1YZazUAPFOiaIFljWDhCbrowserstackUser",
"1YZazUAPFOiaIFljWDhCtruffle-security91",
"cK1bq7JREJtMf1meaGgsbrowserstackUser",
},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{BS_USERNAME Q8fo0ADq_-_Cj4HtE4Gr}{BROWSERSTACK_ACCESS_KEY AQAAABAAA 25IQfQKfEm26vKV96nao}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"25IQfQKfEm26vKV96naoQ8fo0ADq_-_Cj4HtE4Gr"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
if browserstackKey, _ := os.GetEnv("ACCESS_KEY"); browserstackKey != "RxLVnOlvj3#V4bh4RBwOd" {
return fmt.Errorf("invalid accessKey: %v expected: %v", browserstackKey, "RxLVnOlvj3#V4bh4RBwOd")
}
if browserstackUser, _ := os.GetEnv("USER_NAME"); browserstackUser != "test" {
return fmt.Errorf("invalid userName: %v", browserstackUser)
}
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/browshot/browshot.go
================================================
package browshot
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"browshot"}) + `\b([a-zA-Z-0-9]{28})\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{"browshot"}
}
// FromData will find and optionally verify Browshot 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Browshot,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBrowshot(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Browshot
}
func (s Scanner) Description() string {
return "Browshot is a service that allows you to take screenshots of web pages from different browsers and devices. Browshot API keys can be used to automate and manage these screenshots."
}
// docs: https://browshot.com/api/documentation#instance_list
func verifyBrowshot(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.browshot.com/api/v1/instance/list?key="+key, nil)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusBadRequest, http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/browshot/browshot_integration_test.go
================================================
//go:build detectors
// +build detectors
package browshot
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBrowshot_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BROWSHOT")
inactiveSecret := testSecrets.MustGetField("BROWSHOT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a browshot secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Browshot,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a browshot secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Browshot,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Browshot.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Browshot.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/browshot/browshot_test.go
================================================
package browshot
import (
"context"
"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"
)
func TestBrowShot_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.browshot.com/v1/instances/list?key=AemQ06R35S1Y8rXnOzYvT8I4-a7u"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
// Check response status
if resp.StatusCode == http.StatusOK {
fmt.Println("Request successful!")
} else {
fmt.Println("Request failed with status:", resp.Status)
}
}
`,
want: []string{"AemQ06R35S1Y8rXnOzYvT8I4-a7u"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{browshot}{browshot AQAAABAAA SyGuw6JXLULnOEDZjiicnTtQ4FA3}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"SyGuw6JXLULnOEDZjiicnTtQ4FA3"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.browshot.com/v1/instances/list?key=2xN7puShxzNf5fZleQt#hTg305l95D3gSD-c^"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/bscscan/bscscan.go
================================================
package bscscan
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bscscan"}) + `\b([0-9A-Z]{34})\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{"bscscan"}
}
// FromData will find and optionally verify Bscscan 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_BscScan,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBscScan(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_BscScan
}
func (s Scanner) Description() string {
return "BscScan is a block explorer and analytics platform for Binance Smart Chain. BscScan API keys can be used to access data from the Binance Smart Chain blockchain."
}
// docs: https://docs.bscscan.com/api-endpoints/accounts
func verifyBscScan(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.bscscan.com/api?module=account&action=balance&address=0x70F657164e5b75689b64B7fd1fA275F334f28e18&apikey="+key, nil)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return false, err
}
body := string(bodyBytes)
if !strings.Contains(body, "NOTOK") {
return true, nil
}
return false, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/bscscan/bscscan_integration_test.go
================================================
//go:build detectors
// +build detectors
package bscscan
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBscscan_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BSCSCAN")
inactiveSecret := testSecrets.MustGetField("BSCSCAN_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bscscan secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BscScan,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bscscan secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BscScan,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Bscscan.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Bscscan.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/bscscan/bscscan_test.go
================================================
package bscscan
import (
"context"
"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"
)
func TestBscScan_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.bscscan.com/v1/resource?apikey=HYZHPP4PBYXCOZAVK4FH55W4MRHYLALPU1"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"HYZHPP4PBYXCOZAVK4FH55W4MRHYLALPU1"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{bscscan}{bscscan AQAAABAAA SLQOD6LO36MN446N44L98FDJR1AS5PYPTR}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"SLQOD6LO36MN446N44L98FDJR1AS5PYPTR"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.bscscan.com/v1/resource?apikey=2xHYZHPP4PBYXCOZAVK4FH55W4MRHYLALPU1thTg303gSD%c^"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/buddyns/buddyns.go
================================================
package buddyns
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"buddyns"}) + `\b([0-9a-z]{40})\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{"buddyns"}
}
// FromData will find and optionally verify Buddyns 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_BuddyNS,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBuddyns(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_BuddyNS
}
func (s Scanner) Description() string {
return "BuddyNS is a DNS hosting service. BuddyNS API keys can be used to manage DNS zones and records."
}
// docs: https://www.buddyns.com/support/api/v2/
func verifyBuddyns(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.buddyns.com/api/v2/zone/", nil)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Token %s", key))
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/buddyns/buddyns_integration_test.go
================================================
//go:build detectors
// +build detectors
package buddyns
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBuddyns_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BUDDYNS")
inactiveSecret := testSecrets.MustGetField("BUDDYNS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a buddyns secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BuddyNS,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a buddyns secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BuddyNS,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Buddyns.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Buddyns.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/buddyns/buddyns_test.go
================================================
package buddyns
import (
"context"
"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"
)
func TestBuddyNs_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
buddynsToken := "kkmvdiolccw4v0tue4lu7l7kmnnb4ao8z25ezink"
req.Header.Set("Authorization", "Token " + buddynsToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"kkmvdiolccw4v0tue4lu7l7kmnnb4ao8z25ezink"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{buddyns}{buddyns AQAAABAAA jqcayapqh1soy2zlfdbs1j4ytn0mpgmeffzsu2yt}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"jqcayapqh1soy2zlfdbs1j4ytn0mpgmeffzsu2yt"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
buddynsToken := "diolccw4v0tue4lu7l7kmnnb4ao8z25ezink305l95D3gSD%c^"
req.Header.Set("Authorization", "Token " + buddynsToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/budibase/budibase.go
================================================
package budibase
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"budibase"}) + `\b([a-f0-9]{32}-[a-f0-9]{78,80})\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{"budibase"}
}
// FromData will find and optionally verify Budibase 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Budibase,
Raw: []byte(resMatch),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyBudibase(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Budibase
}
func (s Scanner) Description() string {
return "Budibase is a low-code platform for creating internal tools. Budibase API keys can be used to access and modify applications and data within the platform."
}
// docs: https://docs.budibase.com/docs/rest
func verifyBudibase(ctx context.Context, client *http.Client, key string) (bool, error) {
// URL: https://docs.budibase.com/reference/appsearch
// API searches for the app with given name, since we only need to check api key, sending any appname will work.
payload := strings.NewReader(`{"name":"qwerty"}`)
req, err := http.NewRequestWithContext(ctx, "POST", "https://budibase.app/api/public/v1/applications/search", payload)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("x-budibase-api-key", key)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/budibase/budibase_integration_test.go
================================================
//go:build detectors
// +build detectors
package budibase
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBudibase_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("BUDIBASE")
inactiveSecret := testSecrets.MustGetField("BUDIBASE_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a budibase secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Budibase,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a budibase secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Budibase,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 403"))
return []detectors.Result{r}
}(),
wantErr: false,
wantVerificationErr: true,
},
{
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,
},
}
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("Budibase.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.want[i].VerificationError(), got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Budibase.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/budibase/budibase_test.go
================================================
package budibase
import (
"context"
"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"
)
func TestBudiBase_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("x-budibase-api-key", "b256def166fcdf4a429a1e83175105d5-fd36f3da1e934bf533cd0e68dbb80ed6a42e1178bd4200428d83e876e7d05e40b21e3a68888f826d")
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"b256def166fcdf4a429a1e83175105d5-fd36f3da1e934bf533cd0e68dbb80ed6a42e1178bd4200428d83e876e7d05e40b21e3a68888f826d"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{budibase}{budibase AQAAABAAA eb72aa19dafbd0166e16299e0bea6a35-96ab88e1b2691be47aa15b343e8e2b5a3be0564b704db9f2812b6e4decde312038c2f3ba00102e}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"eb72aa19dafbd0166e16299e0bea6a35-96ab88e1b2691be47aa15b343e8e2b5a3be0564b704db9f2812b6e4decde312038c2f3ba00102e"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("x-budibase-api-key", "diolccw4v0tue4lu7l7kmnnb4ao8z25ezink305l95D3gSD%c^")
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/bugherd/bugherd.go
================================================
package bugherd
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bugherd"}) + `\b([0-9a-z]{22})\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{"bugherd"}
}
// FromData will find and optionally verify Bugherd 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Bugherd,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBugherd(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Bugherd
}
func (s Scanner) Description() string {
return "Bugherd is a visual feedback and bug tracking tool for websites. Bugherd API keys can be used to access and manage projects, tasks, and feedback data."
}
// docs: https://www.bugherd.com/api_v2
func verifyBugherd(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.bugherd.com/api_v2/projects.json", nil)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.SetBasicAuth(key, "x")
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/bugherd/bugherd_integration_test.go
================================================
//go:build detectors
// +build detectors
package bugherd
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBugherd_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BUGHERD")
inactiveSecret := testSecrets.MustGetField("BUGHERD_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bugherd secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bugherd,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bugherd secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bugherd,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Bugherd.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Bugherd.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/bugherd/bugherd_test.go
================================================
package bugherd
import (
"context"
"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"
)
func TestBugHerd_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bugherdToken := "fisy6bbu6il4x96bekx587"
req.Header.Set("Authorization", "Basic " + buddynsToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"fisy6bbu6il4x96bekx587"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{bugherd}{bugherd AQAAABAAA mx2rxr8ztizo8kytvx1kan}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"mx2rxr8ztizo8kytvx1kan"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bugherdToken := "fisy6bbu+6il4x()96bekx587-7l7kmnnb4ao8z25ezink305l95D3gSD%c^"
req.Header.Set("Authorization", "Basic " + buddynsToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/bugsnag/bugsnag.go
================================================
package bugsnag
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bugsnag"}) + `\b([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})\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{"bugsnag"}
}
// FromData will find and optionally verify Bugsnag 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Bugsnag,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBugsnag(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Bugsnag
}
func (s Scanner) Description() string {
return "Bugsnag is an error monitoring service for web and mobile applications. Bugsnag API keys can be used to report and manage errors."
}
// docs: https://docs.bugsnag.com/api/
func verifyBugsnag(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.bugsnag.com/user/organizations?admin", nil)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("token %s", key))
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/bugsnag/bugsnag_integration_test.go
================================================
//go:build detectors
// +build detectors
package bugsnag
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBugsnag_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BUGSNAG")
inactiveSecret := testSecrets.MustGetField("BUGSNAG_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bugsnag secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bugsnag,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bugsnag secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bugsnag,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Bugsnag.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Bugsnag.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/bugsnag/bugsnag_test.go
================================================
package bugsnag
import (
"context"
"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"
)
func TestBugSnag_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bugsnagToken := "wz9450iu-iewm-jonx-eab8-0ibxwadddm8i"
req.Header.Set("Authorization", "token " + bugsnagToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"wz9450iu-iewm-jonx-eab8-0ibxwadddm8i"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{bugsnag}{bugsnag AQAAABAAA heatep16-k3fw-dflj-ucc1-ay1lu0in3p7k}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"heatep16-k3fw-dflj-ucc1-ay1lu0in3p7k"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bugsnagToken := "%c^wz9450iu-iewm-jonx-eab8-"
req.Header.Set("Authorization", "token " + bugsnagToken)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/buildkite/v1/buildkite.go
================================================
package buildkite
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
type APIResponse struct {
Scopes []string `json:"scopes"`
}
func (s Scanner) Version() int { return 1 }
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"buildkite"}) + `\b([a-z0-9]{40})\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{"buildkite"}
}
// FromData will find and optionally verify Buildkite 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Buildkite,
Raw: []byte(resMatch),
ExtraData: make(map[string]string),
}
if verify {
extraData, isVerified, verificationErr := VerifyBuildKite(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
s1.ExtraData = extraData
if isVerified {
s1.AnalysisInfo = map[string]string{
"key": resMatch,
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Buildkite
}
func (s Scanner) Description() string {
return "Buildkite is a platform for running fast, secure, and scalable continuous integration pipelines. Buildkite API tokens can be used to access and modify pipeline data and configurations."
}
func VerifyBuildKite(ctx context.Context, client *http.Client, secret string) (map[string]string, bool, error) {
// create a request
// api doc: https://buildkite.com/docs/apis/rest-api/access-token#get-the-current-token
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.buildkite.com/v2/access-token", nil)
if err != nil {
return nil, false, err
}
// add authorization header
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", secret))
res, err := client.Do(req)
if err != nil {
return nil, false, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
var response APIResponse
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
return nil, false, err
}
extraData := make(map[string]string)
extraData["scopes"] = strings.Join(response.Scopes, ", ")
return extraData, true, nil
case http.StatusUnauthorized:
return nil, false, nil
default:
return nil, false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
================================================
FILE: pkg/detectors/buildkite/v1/buildkite_integration_test.go
================================================
//go:build detectors
// +build detectors
package buildkite
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBuildkite_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BUILDKITE_TOKEN")
inactiveSecret := testSecrets.MustGetField("BUILDKITE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a buildkite secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Buildkite,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a buildkite secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Buildkite,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Buildkite.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])
}
got[i].Raw = nil
got[i].ExtraData = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Buildkite.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/buildkite/v1/buildkite_test.go
================================================
package buildkite
import (
"context"
"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"
)
func TestBuildKite_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
buildkite_secret := "kimu4axq3jxxdj8un0kpo3ua2ucr05zmhh4de0r6"
req.Header.Set("Authorization", "Bearer " + buildkite_secret)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"kimu4axq3jxxdj8un0kpo3ua2ucr05zmhh4de0r6"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{buildkite}{buildkite AQAAABAAA ssoj8umx032r2f6sintvtw582bwvxymxgifu6gmk}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"ssoj8umx032r2f6sintvtw582bwvxymxgifu6gmk"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
buildkite_secret := "%c^wz9450iu-buildkite_secret-jonx-eab8"
req.Header.Set("Authorization", "Bearer " + buildkite_secret)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/buildkite/v2/buildkite.go
================================================
package buildkitev2
import (
"context"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
v1 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/buildkite/v1"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct{}
func (s Scanner) Version() int { return 2 }
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(bkua_[a-z0-9]{40})\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{"bkua_"}
}
// FromData will find and optionally verify Buildkite 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Buildkite,
Raw: []byte(resMatch),
}
if verify {
extraData, isVerified, verificationErr := v1.VerifyBuildKite(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
s1.ExtraData = extraData
if isVerified {
s1.AnalysisInfo = map[string]string{
"key": resMatch,
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Buildkite
}
func (s Scanner) Description() string {
return "Buildkite is a platform for running fast, secure, and scalable continuous integration and delivery pipelines. Buildkite access tokens can be used to interact with the Buildkite API."
}
================================================
FILE: pkg/detectors/buildkite/v2/buildkite_test.go
================================================
package buildkitev2
import (
"context"
"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"
)
func TestBuildKiteV2_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
buildkite_secret := "bkua_hqlh73m51jtho0jh12wcf2758c8fcdbv05z023ly"
req.Header.Set("Authorization", "Bearer " + buildkite_secret)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"bkua_hqlh73m51jtho0jh12wcf2758c8fcdbv05z023ly"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{}{AQAAABAAA bkua_j8cqyoaodi7z1fzo8u5albtyw4x9gh83yx1m6ien}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"bkua_j8cqyoaodi7z1fzo8u5albtyw4x9gh83yx1m6ien"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
buildkite_secret := "bkua_hqlh73m51jtho0jh12wcf27v05z023ly-jonx-eab8"
req.Header.Set("Authorization", "Bearer " + buildkite_secret)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/buildkite/v2/buildkitev2_integration_test.go
================================================
//go:build detectors
// +build detectors
package buildkitev2
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBuildkite_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BUILDKITEV2_TOKEN")
inactiveSecret := testSecrets.MustGetField("BUILDKITEV2_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a buildkite secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Buildkite,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a buildkite secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Buildkite,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Buildkite.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])
}
got[i].Raw = nil
got[i].ExtraData = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Buildkite.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/bulbul/bulbul.go
================================================
package bulbul
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bulbul"}) + `\b([a-z0-9]{32})\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{"bulbul"}
}
// FromData will find and optionally verify Bulbul 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Bulbul,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyBulbul(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Bulbul
}
func (s Scanner) Description() string {
return "Bulbul is an API service. Bulbul API keys can be used to access and modify data within the service."
}
// docs: https://docs.jungleworks.com/bulbul/bulbul-api-details
func verifyBulbul(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://prod-api.bulbul.io/view_all_users?api_key=%s", key), nil)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return false, err
}
bodyString := string(bodyBytes)
if strings.Contains(bodyString, `"message":"Successful",`) {
return true, nil
} else {
return false, nil
}
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/bulbul/bulbul_integration_test.go
================================================
//go:build detectors
// +build detectors
package bulbul
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBulbul_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BULBUL")
inactiveSecret := testSecrets.MustGetField("BULBUL_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bulbul secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bulbul,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bulbul secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bulbul,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Bulbul.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Bulbul.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/bulbul/bulbul_test.go
================================================
package bulbul
import (
"context"
"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"
)
func TestBulBul_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.bulbul.com/v1/users?key=3kx19qpx748ldb75lsjicbs6ipit6ssm"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"3kx19qpx748ldb75lsjicbs6ipit6ssm"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{bulbul}{bulbul AQAAABAAA r9gk8o0ctd4xq4r66d3reahu9ku4i4ht}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"r9gk8o0ctd4xq4r66d3reahu9ku4i4ht"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.bulbul.com/v1/users?key=%c^wz9450iu-3kx19qcbs6ipit6ssm-jonx-eab8"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/bulksms/bulksms.go
================================================
package bulksms
import (
"context"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bulksms"}) + `\b([a-zA-Z0-9!@#$%^&*()]{29})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bulksms"}) + `\b([A-F0-9-]{37})\b`)
)
// Keywords are used for efficiently pre-filtering chunks.
func (s Scanner) Keywords() []string {
return []string{"bulksms"}
}
// FromData will find and optionally verify Bulksms 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)
var uniqueIds = make(map[string]struct{})
var uniqueKeys = make(map[string]struct{})
for _, match := range idPat.FindAllStringSubmatch(dataStr, -1) {
uniqueIds[match[1]] = struct{}{}
}
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[match[1]] = struct{}{}
}
for id := range uniqueIds {
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Bulksms,
Raw: []byte(key),
RawV2: []byte(key + id),
}
if verify {
isVerified, verificationErr := verifyBulksms(ctx, client, id, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Bulksms
}
func (s Scanner) Description() string {
return "BulkSMS is a service used for sending SMS messages in bulk. BulkSMS credentials can be used to access and send messages through the BulkSMS API."
}
// docs: https://www.bulksms.com/developer/json/v1/
func verifyBulksms(ctx context.Context, client *http.Client, id, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.bulksms.com/v1/messages", nil)
if err != nil {
return false, err
}
req.SetBasicAuth(id, key)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/bulksms/bulksms_integration_test.go
================================================
//go:build detectors
// +build detectors
package bulksms
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestBulksms_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BULKSMS")
inactiveSecret := testSecrets.MustGetField("BULKSMS_INACTIVE")
token := testSecrets.MustGetField("BULKSMS_TOKEN")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bulksms secret %s within bulksms %s", secret, token)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bulksms,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a bulksms secret %s within bulksms but %s not valid", inactiveSecret, token)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Bulksms,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Bulksms.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])
}
got[i].Raw = nil
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Bulksms.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/bulksms/bulksms_test.go
================================================
package bulksms
import (
"context"
"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"
)
func TestBulkSMS_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bulksmsKey := "(QGxPqRyzvt%xEKcVePJGn)k0d9a"
bulksmsID := "381A26C47380B85F2DB572314-ACBDC267B-8"
req.SetBasicAuth(bulksmsKey, bulksmsID)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"QGxPqRyzvt%xEKcVePJGn)k0d9a381A26C47380B85F2DB572314-ACBDC267B-8"},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{bulksms fXHnHK&cN8H!1r5ersTDIe6ZJ8j51}{bulksms AQAAABAAA 04A1ED4D90D3E17-3968-66B6A571D--2134E}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"fXHnHK&cN8H!1r5ersTDIe6ZJ8j5104A1ED4D90D3E17-3968-66B6A571D--2134E"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.example.com/v1/resource"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
bulksmsKey := "(QGxPqRyzvt%xEKcVePJGn)k0d9a"
bulksmsID := "%c^wz9450iu-iewm-jonx-eab8-/F2DB572314-ACBDC267B-8"
req.SetBasicAuth(bulksmsKey, bulksmsID)
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/buttercms/buttercms.go
================================================
package buttercms
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"buttercms"}) + `\b([a-z0-9]{40})\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{"buttercms"}
}
// FromData will find and optionally verify ButterCMS 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ButterCMS,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyButterCMS(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ButterCMS
}
func (s Scanner) Description() string {
return "ButterCMS is a headless CMS that enables developers to build websites and applications with a content management system. The API keys can be used to access and modify content stored in ButterCMS."
}
// docs: https://buttercms.com/docs/api/#introduction
func verifyButterCMS(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.buttercms.com/v2/posts/?auth_token="+key, nil)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/buttercms/buttercms_integration_test.go
================================================
//go:build detectors
// +build detectors
package buttercms
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestButterCMS_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("BUTTERCMS_TOKEN")
inactiveSecret := testSecrets.MustGetField("BUTTERCMS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a buttercms secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ButterCMS,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a buttercms secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ButterCMS,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("ButterCMS.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("ButterCMS.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/buttercms/buttercms_test.go
================================================
package buttercms
import (
"context"
"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"
)
func TestButterCMS_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
func main() {
url := "https://api.buttercms.com/v2/posts?auth_token=l7psk7wkedkpiyp4jrx5fjdnno8c89243of6yde8"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: []string{"l7psk7wkedkpiyp4jrx5fjdnno8c89243of6yde8"},
},
{
name: "valid pattern - xml",
input: `
GLOBALapi-key{buttercms AQAAABAAA xjndr06i2jiaoqs2plf8x0cgfz976blm1dctjqv9}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"xjndr06i2jiaoqs2plf8x0cgfz976blm1dctjqv9"},
},
{
name: "invalid pattern",
input: `
func main() {
url := "https://api.buttercms.com/v2/posts?auth_token=l7psk7wkedkpiyp4j(rx5fjdnn)"
// Create a new request with the secret as a header
req, err := http.NewRequest("GET", url, http.NoBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// Perform the request
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
`,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)
if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, 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)
}
})
}
}
================================================
FILE: pkg/detectors/caflou/caflou.go
================================================
package caflou
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClientTimeOut(time.Second * 10)
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(eyJhbGciOiJIUzI1NiJ9[a-zA-Z0-9._-]{135})\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{"caflou"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Caflou
}
func (s Scanner) Description() string {
return "Caflou is a business management software used for managing projects, tasks, and finances. Caflou API keys can be used to access and modify this data."
}
// FromData will find and optionally verify Caflou 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Caflou,
Raw: []byte(resMatch),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyCaflou(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyCaflou(ctx context.Context, client *http.Client, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://app.caflou.com/api/v1/accounts", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/caflou/caflou_integration_test.go
================================================
//go:build detectors
// +build detectors
package caflou
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCaflou_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CAFLOU")
inactiveSecret := testSecrets.MustGetField("CAFLOU_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a caflou secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Caflou,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a caflou secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Caflou,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Caflou.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Caflou.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/caflou/caflou_test.go
================================================
package caflou
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
func TestCaflou_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
base_url: "https://api.example.com/instances"
api_key: $API_KEY
caflou_auth_token: "Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX9lkIjo1OTQ5MCwianCpIjoiOTQwZjBlODkxNPhhZjM4OTQ1OGQwMDIxIiziZXhwIjoxGzU1MTk4MDAwfQ.EMNGCPX7aNIvriX360oLFAgMwHeXxKD7N4kdcJtPqTI"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`,
want: []string{"eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX9lkIjo1OTQ5MCwianCpIjoiOTQwZjBlODkxNPhhZjM4OTQ1OGQwMDIxIiziZXhwIjoxGzU1MTk4MDAwfQ.EMNGCPX7aNIvriX360oLFAgMwHeXxKD7N4kdcJtPqTI"},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/calendarific/calendarific.go
================================================
package calendarific
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"calendarific"}) + `\b([a-zA-Z0-9]{32})\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{"calendarific"}
}
// FromData will find and optionally verify Calendarific 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Calendarific,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://calendarific.com/api/v2/holidays?&api_key="+resMatch+"&country=US&year=2019", nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Calendarific
}
func (s Scanner) Description() string {
return "Calendarific provides a public API for obtaining holiday information. The API key can be used to access holiday data for various countries and years."
}
================================================
FILE: pkg/detectors/calendarific/calendarific_integration_test.go
================================================
//go:build detectors
// +build detectors
package calendarific
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCalendarific_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CALENDARIFIC_TOKEN")
inactiveSecret := testSecrets.MustGetField("CALENDARIFIC_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a calendarific secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Calendarific,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a calendarific secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Calendarific,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Calendarific.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Calendarific.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/calendarific/calendarific_test.go
================================================
package calendarific
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
calendarific_api_key: "M9qT0uymrS2FgJ78p9CpLIlFOBLUtmao"
base_url: "https://api.calendarific.com/v1/holidays?api_key=$calendarific_api_key"
auth_token: ""
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "M9qT0uymrS2FgJ78p9CpLIlFOBLUtmao"
)
func TestCalendarific_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/calendlyapikey/calendlyapikey.go
================================================
package calendlyapikey
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"calendly"}) + `\b(eyJ[A-Za-z0-9-_]{100,300}\.eyJ[A-Za-z0-9-_]{100,300}\.[A-Za-z0-9-_]+)\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{"calendly"}
}
// FromData will find and optionally verify CalendlyApiKey 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CalendlyApiKey,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.calendly.com/users/me", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CalendlyApiKey
}
func (s Scanner) Description() string {
return "Calendly is an online scheduling tool that allows users to schedule meetings and appointments. Calendly API keys can be used to access and manage Calendly accounts and data."
}
================================================
FILE: pkg/detectors/calendlyapikey/calendlyapikey_integration_test.go
================================================
//go:build detectors
// +build detectors
package calendlyapikey
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCalendlyApiKey_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CALENDLYAPIKEY_TOKEN")
inactiveSecret := testSecrets.MustGetField("CALENDLYAPIKEY_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a calendlyapikey secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CalendlyApiKey,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a calendlyapikey secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CalendlyApiKey,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CalendlyApiKey.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CalendlyApiKey.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/calendlyapikey/calendlyapikey_test.go
================================================
package calendlyapikey
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
api_key: $API_KEY
base_url: "https://api.example.com/v1/user"
calendly_auth_token: "Bearer eyJuL_8UF5AiQVfO2xlr_HaoSluHz9u-Q-s1qWDWvycQhR11J9wZTmYfFpKnawuIbKjA4i340DSpYI3d3E-oEZZdcHW4cLd_OASWu-H.eyJuOUikPwjw1RKXYXfcjjeqQwdWzA4uooei_ADIUX3of4UzwTjSaaEzLWMGopW4n9Fma0nINBD1qUp_OtbhuH6dmHyv94IeX-hUYla.A0rTdrx3sJ"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "eyJuL_8UF5AiQVfO2xlr_HaoSluHz9u-Q-s1qWDWvycQhR11J9wZTmYfFpKnawuIbKjA4i340DSpYI3d3E-oEZZdcHW4cLd_OASWu-H.eyJuOUikPwjw1RKXYXfcjjeqQwdWzA4uooei_ADIUX3of4UzwTjSaaEzLWMGopW4n9Fma0nINBD1qUp_OtbhuH6dmHyv94IeX-hUYla.A0rTdrx3sJ"
)
func TestCalendlyAPIKey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/calorieninja/calorieninja.go
================================================
package calorieninja
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"calorieninja"}) + `\b([0-9A-Za-z]{40})\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{"calorieninja"}
}
// FromData will find and optionally verify Calorieninja 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CalorieNinja,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.calorieninjas.com/v1/nutrition?query", nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-Api-Key", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CalorieNinja
}
func (s Scanner) Description() string {
return "CalorieNinja is a service that provides nutritional information for various foods. CalorieNinja API keys can be used to access this nutritional data."
}
================================================
FILE: pkg/detectors/calorieninja/calorieninja_integration_test.go
================================================
//go:build detectors
// +build detectors
package calorieninja
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCalorieninja_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CALORIENINJA")
inactiveSecret := testSecrets.MustGetField("CALORIENINJA_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a calorieninja secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CalorieNinja,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a calorieninja secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CalorieNinja,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Calorieninja.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Calorieninja.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/calorieninja/calorieninja_test.go
================================================
package calorieninja
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
calorieninja_api_key: "ix1aaifujilTcGEjB67e1EBBRXcr7r9cdChAR5hb"
base_url: "https://api.example.com/v1/user"
auth_token: ""
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "ix1aaifujilTcGEjB67e1EBBRXcr7r9cdChAR5hb"
)
func TestCalorieNinja_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/campayn/campayn.go
================================================
package campayn
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"campayn"}) + `\b([a-z0-9]{64})\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{"campayn"}
}
// FromData will find and optionally verify Campayn 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Campayn,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://campayn.com/api/v1/lists", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", "TRUEREST apikey="+resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Campayn
}
func (s Scanner) Description() string {
return "Campayn is an email marketing service that allows users to create, send, and track email campaigns. Campayn API keys can be used to manage email lists, send emails, and track campaign performance."
}
================================================
FILE: pkg/detectors/campayn/campayn_integration_test.go
================================================
//go:build detectors
// +build detectors
package campayn
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCampayn_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CAMPAYN_TOKEN")
inactiveSecret := testSecrets.MustGetField("CAMPAYN_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a campayn secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Campayn,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a campayn secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Campayn,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Campayn.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Campayn.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/campayn/campayn_test.go
================================================
package campayn
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
campayn_api_key: "z6q8z47eu46wri18ygu6pc68vsizprt83jrlu5ustliyhktzoxbzhf1ycdaka978"
base_url: "https://api.example.com/v1/user"
auth_token: ""
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "z6q8z47eu46wri18ygu6pc68vsizprt83jrlu5ustliyhktzoxbzhf1ycdaka978"
)
func TestCampayn_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/cannyio/cannyio.go
================================================
package cannyio
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"canny"}) + `\b([a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})\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{"canny"}
}
// FromData will find and optionally verify CannyIo 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CannyIo,
Raw: []byte(resMatch),
}
if verify {
payload := strings.NewReader("apiKey=" + resMatch)
req, err := http.NewRequestWithContext(ctx, "POST", "https://canny.io/api/v1/boards/list", payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CannyIo
}
func (s Scanner) Description() string {
return "Canny is a user feedback tool that helps you track and prioritize feature requests. Canny API keys can be used to access and manage feedback boards and other related data."
}
================================================
FILE: pkg/detectors/cannyio/cannyio_integration_test.go
================================================
//go:build detectors
// +build detectors
package cannyio
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCannyIo_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CANNYIO_TOKEN")
inactiveSecret := testSecrets.MustGetField("CANNYIO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cannyio secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CannyIo,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cannyio secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CannyIo,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CannyIo.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CannyIo.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/cannyio/cannyio_test.go
================================================
package cannyio
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
cannyio_api_key: "faiiahli-goke-db0r-oxli-s20dgab9a0iv"
base_url: "https://api.example.com/v1/user"
auth_token: ""
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "faiiahli-goke-db0r-oxli-s20dgab9a0iv"
)
func TestCannyio_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/capsulecrm/capsulecrm.go
================================================
package capsulecrm
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"capsulecrm"}) + `\b([a-zA-Z0-9-._+=]{64})\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{"capsulecrm"}
}
// FromData will find and optionally verify CapsuleCRM 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CapsuleCRM,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.capsulecrm.com/api/v2/users", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CapsuleCRM
}
func (s Scanner) Description() string {
return "CapsuleCRM is a customer relationship management (CRM) platform. CapsuleCRM API keys can be used to access and manage customer data and interactions."
}
================================================
FILE: pkg/detectors/capsulecrm/capsulecrm_integration_test.go
================================================
//go:build detectors
// +build detectors
package capsulecrm
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCapsuleCRM_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CAPSULECRM")
inactiveSecret := testSecrets.MustGetField("CAPSULECRM_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a capsulecrm secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CapsuleCRM,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a capsulecrm secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CapsuleCRM,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CapsuleCRM.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CapsuleCRM.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/capsulecrm/capsulecrm_test.go
================================================
package capsulecrm
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
api_key: ""
base_url: "https://api.example.com/v1/user"
capsulecrm_auth_token: "Bearer ULeIqU-4ss+YImZYsyjPLSsm.9H.SZJ1v.KxT1D-zbaW6sg5b0R5g=koBH4X62hC"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "ULeIqU-4ss+YImZYsyjPLSsm.9H.SZJ1v.KxT1D-zbaW6sg5b0R5g=koBH4X62hC"
)
func TestCapsulecrm_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/captaindata/v1/captaindata.go
================================================
package captaindata
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
func (s Scanner) Version() int { return 1 }
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"captaindata"}) + `\b([0-9a-f]{64})\b`)
projIdPat = regexp.MustCompile(detectors.PrefixRegex([]string{"captaindata"}) + `\b([0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12})\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{"captaindata"}
}
// FromData will find and optionally verify CaptainData 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
projIdMatches := projIdPat.FindAllStringSubmatch(dataStr, -1)
for _, projIdMatch := range projIdMatches {
resProjIdMatch := strings.TrimSpace(projIdMatch[1])
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CaptainData,
Raw: []byte(resMatch),
RawV2: []byte(resProjIdMatch + resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.captaindata.co/v2/"+resProjIdMatch, nil)
if err != nil {
continue
}
req.Header.Add("x-api-key", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CaptainData
}
func (s Scanner) Description() string {
return "CaptainData is a service for automating data extraction and processing. The API keys can be used to access and control these automation processes."
}
================================================
FILE: pkg/detectors/captaindata/v1/captaindata_integration_test.go
================================================
//go:build detectors
// +build detectors
package captaindata
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCaptainData_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
projId := testSecrets.MustGetField("CAPTAINDATA_PROJID")
secret := testSecrets.MustGetField("CAPTAINDATA")
inactiveSecret := testSecrets.MustGetField("CAPTAINDATA_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a captaindata project %s with captaindata secret %s within", projId, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CaptainData,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a captaindata project %s with captaindata secret %s within but not valid", projId, inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CaptainData,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CaptainData.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CaptainData.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/captaindata/v1/captaindata_test.go
================================================
package captaindata
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
func TestCaptainData_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "typical pattern",
input: "captaindata_project = '12345678-1234-1234-1234-123456789012' captaindata_api_key = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'",
want: []string{"12345678-1234-1234-1234-1234567890121234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"},
},
{
name: "finds all matches",
input: `captaindata_project1 = '12345678-1234-1234-1234-123456789012' captaindata_api_key1 = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
captaindata_project2 = '87654321-4321-4321-4321-210987654321' captaindata_api_key2 = 'fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321'`,
want: []string{
"12345678-1234-1234-1234-1234567890121234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"12345678-1234-1234-1234-123456789012fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321",
"87654321-4321-4321-4321-210987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321",
"87654321-4321-4321-4321-2109876543211234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
},
},
{
name: "invalid pattern",
input: "captaindata_project = '123456' captaindata_api_key = '1234567890'",
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)
}
})
}
}
================================================
FILE: pkg/detectors/captaindata/v2/captaindata.go
================================================
package captaindata
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)
func (Scanner) Version() int { return 2 }
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"captaindata"}) + `\b([0-9a-f]{64})\b`)
projIdPat = regexp.MustCompile(detectors.PrefixRegex([]string{"captaindata"}) + `\b([0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12})\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{"captaindata"}
}
// FromData will find and optionally verify CaptainData 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{}{}
}
uniqueProjIdMatches := make(map[string]struct{})
for _, match := range projIdPat.FindAllStringSubmatch(dataStr, -1) {
uniqueProjIdMatches[match[1]] = struct{}{}
}
for projId := range uniqueProjIdMatches {
for apiKey := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CaptainData,
Raw: []byte(apiKey),
RawV2: []byte(projId + apiKey),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, extraData, verificationErr := verifyMatch(ctx, client, projId, apiKey)
s1.Verified = isVerified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr, apiKey)
}
results = append(results, s1)
}
}
return
}
func verifyMatch(ctx context.Context, client *http.Client, projId, apiKey string) (bool, map[string]string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.captaindata.co/v3/workspace", nil)
if err != nil {
return false, nil, err
}
req.Header.Set("Authorization", "x-api-key "+apiKey)
req.Header.Set("x-project-id", projId)
res, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
return true, nil, nil
case http.StatusUnauthorized:
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CaptainData
}
func (s Scanner) Description() string {
return "CaptainData is a service for automating data extraction and processing. The API keys can be used to access and control these automation processes."
}
================================================
FILE: pkg/detectors/captaindata/v2/captaindata_integration_test.go
================================================
//go:build detectors
// +build detectors
package captaindata
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/detectorspb"
)
func TestCaptainData_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
projId := testSecrets.MustGetField("CAPTAINDATA_PROJID")
secret := testSecrets.MustGetField("CAPTAINDATA")
inactiveSecret := testSecrets.MustGetField("CAPTAINDATA_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a captaindata project %s with captaindata secret %s within", projId, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CaptainData,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a captaindata project %s with captaindata secret %s within but not valid", projId, inactiveSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CaptainData,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: true,
},
{
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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CaptainData.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "ExtraData", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("CaptainData.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/captaindata/v2/captaindata_test.go
================================================
package captaindata
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
func TestCaptainData_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "typical pattern",
input: "captaindata_project = '12345678-1234-1234-1234-123456789012' captaindata_api_key = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'",
want: []string{"12345678-1234-1234-1234-1234567890121234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"},
},
{
name: "finds all matches",
input: `captaindata_project1 = '12345678-1234-1234-1234-123456789012' captaindata_api_key1 = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
captaindata_project2 = '87654321-4321-4321-4321-210987654321' captaindata_api_key2 = 'fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321'`,
want: []string{
"12345678-1234-1234-1234-1234567890121234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"12345678-1234-1234-1234-123456789012fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321",
"87654321-4321-4321-4321-210987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321",
"87654321-4321-4321-4321-2109876543211234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
},
},
{
name: "invalid pattern",
input: "captaindata_project = '123456' captaindata_api_key = '1234567890'",
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)
}
})
}
}
================================================
FILE: pkg/detectors/carboninterface/carboninterface.go
================================================
package carboninterface
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"carboninterface"}) + `\b([a-zA-Z0-9]{21})\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{"carboninterface"}
}
// FromData will find and optionally verify CarbonInterface 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CarbonInterface,
Raw: []byte(resMatch),
}
if verify {
payload := strings.NewReader(`{"type":"flight","passengers":2,"legs":[{"departure_airport":"sfo","destination_airport":"yyz"},{"departure_airport":"yyz","destination_airport":"sfo"}]}`)
req, err := http.NewRequestWithContext(ctx, "POST", "https://www.carboninterface.com/api/v1/estimates", payload)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
req.Header.Add("Content-type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CarbonInterface
}
func (s Scanner) Description() string {
return "CarbonInterface provides an API for estimating carbon emissions for various activities. The API keys can be used to access and utilize this service."
}
================================================
FILE: pkg/detectors/carboninterface/carboninterface_integration_test.go
================================================
//go:build detectors
// +build detectors
package carboninterface
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCarbonInterface_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CARBONINTERFACE")
inactiveSecret := testSecrets.MustGetField("CARBONINTERFACE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a carboninterface secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CarbonInterface,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a carboninterface secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CarbonInterface,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CarbonInterface.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CarbonInterface.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/carboninterface/carboninterface_test.go
================================================
package carboninterface
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
api_key: ""
base_url: "https://api.example.com/v1/user"
carboninterface_auth_token: "Bearer PkN3gWaSHSIjz188TjD4h"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "PkN3gWaSHSIjz188TjD4h"
)
func TestCarbonInterface_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/cashboard/cashboard.go
================================================
package cashboard
import (
"context"
b64 "encoding/base64"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cashboard"}) + `\b([0-9A-Z]{3}-[0-9A-Z]{3}-[0-9A-Z]{3}-[0-9A-Z]{3})\b`)
userPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cashboard"}) + `\b([0-9a-z]{1,})\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{"cashboard"}
}
// FromData will find and optionally verify Cashboard 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
userMatches := userPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, userMatch := range userMatches {
resUser := strings.TrimSpace(userMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Cashboard,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resUser),
}
if verify {
data := fmt.Sprintf("%s:%s", resUser, resMatch)
sEnc := b64.StdEncoding.EncodeToString([]byte(data))
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cashboardapp.com/account.xml", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Cashboard
}
func (s Scanner) Description() string {
return "Cashboard is a financial management service. Cashboard credentials can be used to access and manage financial data and accounts."
}
================================================
FILE: pkg/detectors/cashboard/cashboard_integration_test.go
================================================
//go:build detectors
// +build detectors
package cashboard
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCashboard_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CASHBOARD")
user := testSecrets.MustGetField("SCANNER_USERNAME")
inactiveSecret := testSecrets.MustGetField("CASHBOARD_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cashboard secret %s within %s", secret, user)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cashboard,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cashboard secret %s within %s but not valid", inactiveSecret, user)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cashboard,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Cashboard.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Cashboard.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/cashboard/cashboard_test.go
================================================
package cashboard
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
cashboard_key: "F1A-NEI-HY4-PZK"
cashboard_user: "ts7z"
auth_type: Basic
base_url: "https://api.example.com/v1/user"
auth_token: ""
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "F1A-NEI-HY4-PZKts7z"
)
func TestCashBoard_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/caspio/caspio.go
================================================
package caspio
import (
"context"
"fmt"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = detectors.DetectorHttpClientWithNoLocalAddresses
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"caspio"}) + `\b([a-z0-9]{50})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"caspio"}) + `\b([a-z0-9]{50})\b`)
domainPat = regexp.MustCompile(detectors.PrefixRegex([]string{"caspio"}) + `\b([a-z0-9]{8})\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{"caspio"}
}
// FromData will find and optionally verify Caspio 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
domainMatches := domainPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
resIdMatch := strings.TrimSpace(idMatch[1])
for _, domainMatch := range domainMatches {
resDomainMatch := strings.TrimSpace(domainMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Caspio,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch + resDomainMatch),
}
if verify {
payload := strings.NewReader(fmt.Sprintf(`grant_type=client_credentials&client_id=%s&client_secret=%s`, resIdMatch, resMatch))
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("https://%s.caspio.com/oauth/token", resDomainMatch), payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "text/plain")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Caspio
}
func (s Scanner) Description() string {
return "Caspio is a cloud platform for building custom database applications. Caspio credentials can be used to access and manage these applications."
}
================================================
FILE: pkg/detectors/caspio/caspio_integration_test.go
================================================
//go:build detectors
// +build detectors
package caspio
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCaspio_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CASPIO")
id := testSecrets.MustGetField("CASPIO_ID")
subdomain := testSecrets.MustGetField("CASPIO_SUBDOMAIN")
inactiveSecret := testSecrets.MustGetField("CASPIO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a caspio secret %s within caspio %s and caspio subdomain %s", secret, id, subdomain)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Caspio,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a caspio secret %s within caspio %s and caspio subdomain %s but not valid", inactiveSecret, id, subdomain)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Caspio,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Caspio.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Caspio.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/caspio/caspio_test.go
================================================
package caspio
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
caspio_id: "qye6c979b55w55hz7nqrdgax3pufdsigwvhofsrxalgk01asty"
caspio_secret: "x5vzi1o2fmnjywilv3obwx5n041f56v54lgral6znyccet8ibe"
caspio_domain: xlo0xo2s
auth_type: Client Credentials
base_url: "https://$caspio_domain.caspio.com/v1/user"
auth_token: ""
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secrets = []string{
"qye6c979b55w55hz7nqrdgax3pufdsigwvhofsrxalgk01astyqye6c979b55w55hz7nqrdgax3pufdsigwvhofsrxalgk01astyxlo0xo2s",
"qye6c979b55w55hz7nqrdgax3pufdsigwvhofsrxalgk01astyx5vzi1o2fmnjywilv3obwx5n041f56v54lgral6znyccet8ibexlo0xo2s",
"x5vzi1o2fmnjywilv3obwx5n041f56v54lgral6znyccet8ibeqye6c979b55w55hz7nqrdgax3pufdsigwvhofsrxalgk01astyxlo0xo2s",
"x5vzi1o2fmnjywilv3obwx5n041f56v54lgral6znyccet8ibex5vzi1o2fmnjywilv3obwx5n041f56v54lgral6znyccet8ibexlo0xo2s",
}
)
func TestCaspio_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: secrets,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/censys/censys.go
================================================
package censys
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"censys"}) + `\b([a-zA-Z0-9]{32})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"censys"}) + `\b([a-z0-9-]{36})\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{"censys"}
}
// FromData will find and optionally verify Censys 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
tokenPatMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
userPatMatch := strings.TrimSpace(idMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Censys,
Raw: []byte(tokenPatMatch),
RawV2: []byte(tokenPatMatch + userPatMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://search.censys.io/api/v1/account", nil)
if err != nil {
continue
}
req.SetBasicAuth(userPatMatch, tokenPatMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Censys
}
func (s Scanner) Description() string {
return "Censys is a search engine that enables researchers to ask questions about the hosts and networks that compose the Internet. Censys API keys can be used to access and query this data."
}
================================================
FILE: pkg/detectors/censys/censys_integration_test.go
================================================
//go:build detectors
// +build detectors
package censys
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCensys_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CENSYS")
id := testSecrets.MustGetField("CENSYS_ID")
inactiveSecret := testSecrets.MustGetField("CENSYS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a censys secret %s within censys %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Censys,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a censys secret %s within censys %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Censys,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Censys.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])
}
got[i].Raw = nil
if len(got[i].RawV2) == 0 {
t.Fatalf("no raw v2 secret present: \n %+v", got[i])
}
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Censys.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/censys/censys_test.go
================================================
package censys
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
censys_key: "roLtT01S6znCRhgoSwNNKY8O7AELn8e4"
censys_user: "p4cuaz9fuonwmbfkc4di5uqsizp4yyttpu-q"
auth_type: Basic
base_url: "https://api.example.com/v1/user"
auth_token: ""
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "roLtT01S6znCRhgoSwNNKY8O7AELn8e4p4cuaz9fuonwmbfkc4di5uqsizp4yyttpu-q"
)
func TestCensys_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/centralstationcrm/centralstationcrm.go
================================================
package centralstationcrm
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"centralstation"}) + `\b([a-z0-9]{30})\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{"centralstationcrm"}
}
// FromData will find and optionally verify CentralStationCRM 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CentralStationCRM,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.centralstationcrm.net/api/users.json", nil)
if err != nil {
continue
}
req.Header.Add("X-apikey", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CentralStationCRM
}
func (s Scanner) Description() string {
return "CentralStationCRM is a customer relationship management service. The API keys can be used to access and manage customer data."
}
================================================
FILE: pkg/detectors/centralstationcrm/centralstationcrm_integration_test.go
================================================
//go:build detectors
// +build detectors
package centralstationcrm
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCentralStationCRM_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CENTRALSTATIONCRM")
inactiveSecret := testSecrets.MustGetField("CENTRALSTATIONCRM_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a centralstationcrm secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CentralStationCRM,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a centralstationcrm secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CentralStationCRM,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CentralStationCRM.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CentralStationCRM.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/centralstationcrm/centralstationcrm_test.go
================================================
package centralstationcrm
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
centralstationcrm_api_key: "gyeyy7soy4lxx7yenw56iba4szfu1f"
base_url: "https://api.example.com/v1/user"
auth_token: ""
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "gyeyy7soy4lxx7yenw56iba4szfu1f"
)
func TestCentralStationCRM_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/cexio/cexio.go
================================================
package cexio
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cexio", "cex.io"}) + `\b([0-9A-Za-z]{24,27})\b`)
secretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cexio", "cex.io"}) + `\b([0-9A-Za-z]{24,27})\b`)
userIdPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cexio", "cex.io"}) + `\b([a-z]{2}[0-9]{9})\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{"cexio", "cex.io"}
}
// FromData will find and optionally verify CexIO 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)
keyMatches := keyPat.FindAllStringSubmatch(dataStr, -1)
secretMatches := secretPat.FindAllStringSubmatch(dataStr, -1)
userIdMatches := userIdPat.FindAllStringSubmatch(dataStr, -1)
for _, userIdMatch := range userIdMatches {
resUserIdMatch := strings.TrimSpace(userIdMatch[1])
for _, keyMatch := range keyMatches {
resKeyMatch := strings.TrimSpace(keyMatch[1])
for _, secretMatch := range secretMatches {
resSecretMatch := strings.TrimSpace(secretMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CexIO,
Raw: []byte(resKeyMatch),
RawV2: []byte(resUserIdMatch + resSecretMatch),
}
if verify {
timestamp := strconv.FormatInt(time.Now().Unix()*1000, 10)
signature := getCexIOPassphrase(resSecretMatch, resKeyMatch, timestamp, resUserIdMatch)
payload := url.Values{}
payload.Add("key", resKeyMatch)
payload.Add("signature", signature)
payload.Add("nonce", timestamp)
req, err := http.NewRequestWithContext(ctx, "POST", "https://cex.io/api/balance/", strings.NewReader(payload.Encode()))
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
continue
}
bodyString := string(body)
validResponse := strings.Contains(bodyString, `timestamp`)
var responseObject Response
if err := json.Unmarshal(body, &responseObject); err != nil {
continue
}
if res.StatusCode >= 200 && res.StatusCode < 300 && validResponse {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
}
return results, nil
}
type Response struct {
Error string `json:"error"`
}
func getCexIOPassphrase(apiSecret string, apiKey string, nonce string, userId string) string {
msg := nonce + userId + apiKey
mac := hmac.New(sha256.New, []byte(apiSecret))
mac.Write([]byte(msg))
macsum := mac.Sum(nil)
return strings.ToUpper(hex.EncodeToString(macsum))
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CexIO
}
func (s Scanner) Description() string {
return "CexIO is a cryptocurrency exchange platform. CexIO API keys can be used to access and manage cryptocurrency accounts and transactions."
}
================================================
FILE: pkg/detectors/cexio/cexio_integration_test.go
================================================
//go:build detectors
// +build detectors
package cexio
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCexIO_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
userId := testSecrets.MustGetField("CEXIO_USERID")
key := testSecrets.MustGetField("CEXIO_KEY")
inactiveKey := testSecrets.MustGetField("CEXIO_KEY_INACTIVE")
secret := testSecrets.MustGetField("CEXIO")
inactiveSecret := testSecrets.MustGetField("CEXIO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cexio userId %s with cexio key %s and cexio secret %s within", userId, key, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CexIO,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cexio userId %s with cexio key %s and cexio secret %s within but not valid", userId, inactiveKey, inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CexIO,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CexIO.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CexIO.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/cexio/cexio_test.go
================================================
package cexio
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
cexio_key: "bVI24QF2B8omVr9ddfxSHtkb18D"
cexio_secret: "2m2pEr2OLi48y2NCpSbPVwJqb"
cex.io_userID: "zd930167221"
auth_type: Signature
base_url: "https://api.example.com/v1/user"
auth_token: ""
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secrets = []string{
"zd9301672212m2pEr2OLi48y2NCpSbPVwJqb",
"zd9301672212m2pEr2OLi48y2NCpSbPVwJqb",
"zd930167221bVI24QF2B8omVr9ddfxSHtkb18D",
"zd930167221bVI24QF2B8omVr9ddfxSHtkb18D",
}
)
func TestCexio_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: secrets,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/chartmogul/chartmogul.go
================================================
package chartmogul
import (
"context"
b64 "encoding/base64"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"chartmogul"}) + `\b([a-z0-9]{32})\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{"chartmogul"}
}
// FromData will find and optionally verify Chartmogul 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Chartmogul,
Raw: []byte(resMatch),
}
if verify {
data := fmt.Sprintf("%s:", resMatch)
sEnc := b64.StdEncoding.EncodeToString([]byte(data))
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.chartmogul.com/v1/ping", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Chartmogul
}
func (s Scanner) Description() string {
return "ChartMogul is a subscription analytics platform that helps businesses measure, understand, and grow their subscription revenue. ChartMogul API keys can be used to access and manage subscription data."
}
================================================
FILE: pkg/detectors/chartmogul/chartmogul_integration_test.go
================================================
//go:build detectors
// +build detectors
package chartmogul
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestChartmogul_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CHARTMOGUL")
inactiveSecret := testSecrets.MustGetField("CHARTMOGUL_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a chartmogul secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Chartmogul,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a chartmogul secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Chartmogul,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Chartmogul.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Chartmogul.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/chartmogul/chartmogul_test.go
================================================
package chartmogul
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
chartmogul_key: "e8hwspf91879g0u267yq1bkoxquvwndk"
auth_type: Basic
base_url: "https://api.example.com/v1/user"
auth_token: ""
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "e8hwspf91879g0u267yq1bkoxquvwndk"
)
func TestChartMogul_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/chatbot/chatbot.go
================================================
package chatbot
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"chatbot"}) + `\b([a-zA-Z0-9_]{32})\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{"chatbot"}
}
// FromData will find and optionally verify Chatbot 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Chatbot,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.chatbot.com/stories", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Chatbot
}
func (s Scanner) Description() string {
return "Chatbot API keys are used to interact with the Chatbot service, allowing access to create, modify, and retrieve chatbot stories and other resources."
}
================================================
FILE: pkg/detectors/chatbot/chatbot_integration_test.go
================================================
//go:build detectors
// +build detectors
package chatbot
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestChatbot_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CHATBOT_TOKEN")
inactiveSecret := testSecrets.MustGetField("CHATBOT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a chatbot secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Chatbot,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a chatbot secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Chatbot,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Chatbot.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Chatbot.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/chatbot/chatbot_test.go
================================================
package chatbot
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: Bearer
base_url: "https://api.example.com/v1/user"
chatbot_auth_token: "Bearer 5RzDGrpFKkrA_90yM_BFmyxKKAQkgu0B"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "5RzDGrpFKkrA_90yM_BFmyxKKAQkgu0B"
)
func TestChatBot_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/chatfule/chatfule.go
================================================
package chatfule
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"chatfuel"}) + `\b([a-zA-Z0-9]{128})\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{"chatfuel"}
}
// FromData will find and optionally verify Chatfule 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Chatfule,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://dashboard.chatfuel.com/api/bots", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Chatfule
}
func (s Scanner) Description() string {
return "Chatfuel is a platform for creating chatbots for Facebook Messenger and other platforms. Chatfuel API keys can be used to access and manage chatbot configurations and interactions."
}
================================================
FILE: pkg/detectors/chatfule/chatfule_integration_test.go
================================================
//go:build detectors
// +build detectors
package chatfule
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestChatfule_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CHATFULE")
inactiveSecret := testSecrets.MustGetField("CHATFULE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a chatfuel secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Chatfule,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a chatfuel secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Chatfule,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Chatfuel.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Chatfuel.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/chatfule/chatfule_test.go
================================================
package chatfule
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: Bearer
base_url: "https://api.example.com/v1/user"
chatfuel_auth_token: "Bearer 22ZEyoBNMXpT6rHmbuBlIJ8n19vo3tHzNTQDUku00WhHuBCAlkfkn8kQkXslseKEHARZthTrm8QfErQ5auXEr8teFIt6stHYi9sfJXM7IK0vEsezKFQwADCvMhX202eL"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "22ZEyoBNMXpT6rHmbuBlIJ8n19vo3tHzNTQDUku00WhHuBCAlkfkn8kQkXslseKEHARZthTrm8QfErQ5auXEr8teFIt6stHYi9sfJXM7IK0vEsezKFQwADCvMhX202eL"
)
func TestChatFule_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/checio/checio.go
================================================
package checio
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"checio"}) + `\b(pk_[a-z0-9]{45})\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{"checio"}
}
// FromData will find and optionally verify ChecIO 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ChecIO,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.chec.io/v1/products?limit=25", nil)
if err != nil {
continue
}
req.Header.Add("X-Authorization", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ChecIO
}
func (s Scanner) Description() string {
return "ChecIO is an eCommerce platform that provides APIs for managing products, carts, and orders. ChecIO API keys can be used to access and manage these eCommerce resources."
}
================================================
FILE: pkg/detectors/checio/checio_integration_test.go
================================================
//go:build detectors
// +build detectors
package checio
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestChecIO_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CHECIO")
inactiveSecret := testSecrets.MustGetField("CHECIO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a checio secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ChecIO,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a checio secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ChecIO,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("ChecIO.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("ChecIO.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/checio/checio_test.go
================================================
package checio
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: N/A
base_url: "https://api.example.com/v1/user"
checio_auth_token: "X-Authorization pk_k64v4e7f5vfun5efk7kscnvuwiuo9ioxbvxjq8qrdga0p"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "pk_k64v4e7f5vfun5efk7kscnvuwiuo9ioxbvxjq8qrdga0p"
)
func TestChecio_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/checklyhq/checklyhq.go
================================================
package checklyhq
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"checklyhq"}) + `\b([a-z0-9]{32})\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{"checklyhq"}
}
// FromData will find and optionally verify ChecklyHQ 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ChecklyHQ,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.checklyhq.com/v1/checks", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ChecklyHQ
}
func (s Scanner) Description() string {
return "ChecklyHQ is a monitoring service for API and browser checks. ChecklyHQ API keys can be used to access and manage these checks."
}
================================================
FILE: pkg/detectors/checklyhq/checklyhq_integration_test.go
================================================
//go:build detectors
// +build detectors
package checklyhq
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestChecklyHQ_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CHECKLYHQ")
inactiveSecret := testSecrets.MustGetField("CHECKLYHQ_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a checklyhq secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ChecklyHQ,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a checklyhq secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ChecklyHQ,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("ChecklyHQ.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("ChecklyHQ.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/checklyhq/checklyhq_test.go
================================================
package checklyhq
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: Bearer
base_url: "https://api.example.com/v1/user"
checklyhq_auth_token: "Bearer r3sd5apfe7p3eg1318qpbtxo36gwcct2"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "r3sd5apfe7p3eg1318qpbtxo36gwcct2"
)
func TestChecklyhq_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/checkout/checkout.go
================================================
package checkout
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
// Tokens starting with sk_test are used for the app's sandbox environment while tokens starting with sk only are for production environment
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"checkout"}) + `\b((sk_|sk_test_)[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"checkout"}) + `\b(cus_[0-9a-zA-Z]{26})\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{"checkout"}
}
// FromData will find and optionally verify Checkout 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
resIdMatch := strings.TrimSpace(idMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Checkout,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch),
}
if verify {
// Used the app's sandbox environment for this case since I can't create a live account.
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.sandbox.checkout.com/customers/"+resIdMatch, nil)
if err != nil {
continue
}
req.Header.Add("Authorization", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Checkout
}
func (s Scanner) Description() string {
return "Checkout is a global payment solution provider. Checkout API keys can be used to process payments and manage customer data."
}
================================================
FILE: pkg/detectors/checkout/checkout_integration_test.go
================================================
//go:build detectors
// +build detectors
package checkout
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCheckout_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CHECKOUT")
inactiveSecret := testSecrets.MustGetField("CHECKOUT_INACTIVE")
secretId := testSecrets.MustGetField("CHECKOUT_ID")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a checkout secret %s with checkout id %s within", secret, secretId)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Checkout,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a checkout secret %s with checkout id %s within but not valid", inactiveSecret, secretId)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Checkout,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Checkout.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Checkout.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/checkout/checkout_test.go
================================================
package checkout
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: API-Key
base_url: "https://api.checkout.com/v1/customers/cus_DaZoK0ioakAfFaj6fyqSFQZatk"
checkout_api_key: "sk_test_14a67eEd-21Fd-B18d-2B8D-275697febE7D"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "sk_test_14a67eEd-21Fd-B18d-2B8D-275697febE7Dcus_DaZoK0ioakAfFaj6fyqSFQZatk"
)
func TestCheckout_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/checkvist/checkvist.go
================================================
package checkvist
import (
"context"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"checkvist"}) + `\b([0-9a-zA-Z]{14})\b`)
emailPat = regexp.MustCompile(detectors.PrefixRegex([]string{"checkvist"}) + common.EmailPattern)
)
// 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{"checkvist"}
}
// FromData will find and optionally verify Checkvist 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
uniqueEmailMatches := make(map[string]struct{})
for _, match := range emailPat.FindAllStringSubmatch(dataStr, -1) {
uniqueEmailMatches[strings.TrimSpace(match[1])] = struct{}{}
}
for emailMatch := range uniqueEmailMatches {
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Checkvist,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + emailMatch),
}
if verify {
payload := url.Values{}
payload.Add("username", emailMatch)
payload.Add("remote_key", resMatch)
req, err := http.NewRequestWithContext(ctx, "GET", "https://checkvist.com/auth/login.json?version=2", strings.NewReader(payload.Encode()))
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Checkvist
}
func (s Scanner) Description() string {
return "Checkvist is an online task management tool. The credentials found can be used to access and manage tasks and data within Checkvist."
}
================================================
FILE: pkg/detectors/checkvist/checkvist_integration_test.go
================================================
//go:build detectors
// +build detectors
package checkvist
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCheckvist_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
user := testSecrets.MustGetField("CHECKVIST_EMAIL")
secret := testSecrets.MustGetField("CHECKVIST")
inactiveSecret := testSecrets.MustGetField("CHECKVIST_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a checkvist user %s with checkvist secret %s within", user, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Checkvist,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a checkvist user %s with checkvist secret %s within but not valid", user, inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Checkvist,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Checkvist.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Checkvist.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/checkvist/checkvist_test.go
================================================
package checkvist
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 = "wdvnusa87afxYn / testuser1005@example.com"
invalidPattern = "wdvn-usa87a-fxp9ioasQQsstestUsQQ@example"
)
func TestCheckvist_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: fmt.Sprintf("checkvist: %s", validPattern),
want: []string{"wdvnusa87afxYntestuser1005@example.com"},
},
{
name: "valid pattern - key out of prefix range",
input: fmt.Sprintf("checkvist keyword is not close to the real key and id = %s", validPattern),
want: nil,
},
{
name: "invalid pattern",
input: fmt.Sprintf("checkvist: %s", invalidPattern),
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 && test.want != nil {
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) {
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)
}
})
}
}
================================================
FILE: pkg/detectors/cicero/cicero.go
================================================
package cicero
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cicero"}) + `\b([0-9a-z]{40})\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{"cicero"}
}
// FromData will find and optionally verify Cicero 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Cicero,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://cicero.azavea.com/v3.1/account/credits_remaining?key=%s", resMatch), nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Cicero
}
func (s Scanner) Description() string {
return "Cicero is a service provided by Azavea that offers various geospatial and civic data APIs. Cicero keys can be used to access and interact with these APIs."
}
================================================
FILE: pkg/detectors/cicero/cicero_integration_test.go
================================================
//go:build detectors
// +build detectors
package cicero
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCicero_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CICERO")
inactiveSecret := testSecrets.MustGetField("CICERO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cicero secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cicero,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cicero secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cicero,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Cicero.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Cicero.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/cicero/cicero_test.go
================================================
package cicero
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
base_url: "https://api.cicero.com/v1/user?key=sbxod2yo56quitbyujhkig3mgtu6z49f4hh56va6"
auth_token: ""
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "sbxod2yo56quitbyujhkig3mgtu6z49f4hh56va6"
)
func TestCicero_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/circleci/v1/circleci.go
================================================
package circleci
import (
"context"
"fmt"
"io"
"net/http"
"strconv"
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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"circle"}) + `([a-fA-F0-9]{40})`)
)
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
func (Scanner) Version() int { return 1 }
// 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{"circle"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Circle
}
func (s Scanner) Description() string {
return "CircleCI is a continuous integration and delivery platform used to build, test, and deploy software. CircleCI tokens can be used to interact with the CircleCI API and access various resources and functionalities."
}
// FromData will find and optionally verify Circle 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)
var uniqueTokens = make(map[string]struct{})
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueTokens[match[1]] = struct{}{}
}
for token := range uniqueTokens {
result := detectors.Result{
DetectorType: detectorspb.DetectorType_Circle,
Raw: []byte(token),
ExtraData: map[string]string{
"Version": strconv.Itoa(s.Version()),
},
}
if verify {
// https://circleci.com/docs/api/#authentication
isVerified, verificationErr := VerifyCircleCIToken(ctx, s.getClient(), token)
result.Verified = isVerified
result.SetVerificationError(verificationErr, token)
}
results = append(results, result)
}
return
}
func VerifyCircleCIToken(ctx context.Context, client *http.Client, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://circleci.com/api/v2/me", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Accept", "application/json;")
req.Header.Add("Circle-Token", token)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/circleci/v1/circleci_integration_test.go
================================================
//go:build detectors
// +build detectors
package circleci
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCircleCI_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CIRCLECI")
secretInactive := testSecrets.MustGetField("CIRCLECI_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a circle secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Circle,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a circle secret %s within", secretInactive)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Circle,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CircleCI.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
if os.Getenv("FORCE_PASS_DIFF") == "true" {
return
}
for i := range got {
if len(got[i].Raw) == 0 {
t.Fatal("no raw secret present")
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CircleCI.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/circleci/v1/circleci_test.go
================================================
package circleci
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: API-Key
base_url: "https://api.example.com/v1/user"
circleci_api_key: "4a4fFEA0Cb7760ee42Bb1Dc82b1E4E5eDCacB9E7"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "4a4fFEA0Cb7760ee42Bb1Dc82b1E4E5eDCacB9E7"
)
func TestCircleCI_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/circleci/v2/circleci.go
================================================
package circleci
import (
"context"
"net/http"
"strconv"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
v1 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/circleci/v1"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
keyPat = regexp.MustCompile(`(CCIPAT_[a-zA-Z0-9]{22}_[a-fA-F0-9]{40})`)
)
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
func (Scanner) Version() int { return 2 }
// 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{"CCIPAT_"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Circle
}
func (s Scanner) Description() string {
return "CircleCI is a continuous integration and delivery platform used to build, test, and deploy software. CircleCI tokens can be used to interact with the CircleCI API and access various resources and functionalities."
}
// FromData will find and optionally verify Circle 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)
var uniqueTokens = make(map[string]struct{})
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueTokens[match[1]] = struct{}{}
}
for token := range uniqueTokens {
result := detectors.Result{
DetectorType: detectorspb.DetectorType_Circle,
Raw: []byte(token),
ExtraData: map[string]string{
"Version": strconv.Itoa(s.Version()),
},
}
if verify {
isVerified, verificationErr := v1.VerifyCircleCIToken(ctx, s.getClient(), token)
result.Verified = isVerified
result.SetVerificationError(verificationErr, token)
}
results = append(results, result)
}
return
}
================================================
FILE: pkg/detectors/circleci/v2/circleci_integration_test.go
================================================
//go:build detectors
// +build detectors
package circleci
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCircleCI_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CIRCLECI")
secretInactive := testSecrets.MustGetField("CIRCLECI_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a circle secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Circle,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a circle secret %s within", secretInactive)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Circle,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CircleCI.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
if os.Getenv("FORCE_PASS_DIFF") == "true" {
return
}
for i := range got {
if len(got[i].Raw) == 0 {
t.Fatal("no raw secret present")
}
got[i].Raw = nil
got[i].ExtraData = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CircleCI.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/circleci/v2/circleci_test.go
================================================
package circleci
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
func TestCircleCI_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: API-Key
base_url: "https://api.example.com/v1/user"
api_key: "CCIPAT_FAKEd5qPreGFAKEaQxBi6i_914bf0042f4f2d34e1d2ef6615c051a5caf70172"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`,
want: []string{"CCIPAT_FAKEd5qPreGFAKEaQxBi6i_914bf0042f4f2d34e1d2ef6615c051a5caf70172"},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/clarifai/clarifai.go
================================================
package clarifai
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"clarifai"}) + `\b([a-zA-Z0-9]{32})\b`) // could be an api key tied to an app or a personal access token (pat)
)
// 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{"clarifai"}
}
// FromData will find and optionally verify Clarifai 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Clarifai,
Raw: []byte(resMatch),
}
if verify {
// test for api key
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.clarifai.com/v2/inputs", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Key %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
if !s1.Verified {
// test for pat
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.clarifai.com/v2/users/me", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Key %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Clarifai
}
func (s Scanner) Description() string {
return "Clarifai is an AI platform for visual recognition. Clarifai API keys can be used to access and manage visual recognition models and data."
}
================================================
FILE: pkg/detectors/clarifai/clarifai_integration_test.go
================================================
//go:build detectors
// +build detectors
package clarifai
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestClarifai_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
apiKey := testSecrets.MustGetField("CLARIFAI_API_KEY")
inactiveApiKey := testSecrets.MustGetField("CLARIFAI_API_KEY_INACTIVE")
pat := testSecrets.MustGetField("CLARIFAI_PAT")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clarifai api key %s within", apiKey)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Clarifai,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, verified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clarifai pat %s within", pat)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Clarifai,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clarifai secret %s within but unverified", inactiveApiKey)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Clarifai,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Clarifai.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Clarifai.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/clarifai/clarifai_test.go
================================================
package clarifai
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: API-Key
base_url: "https://api.example.com/v1/user"
clarifai_key: "WCFcfUCsl2P3vCJuFgDuLeUXduycoZli"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "WCFcfUCsl2P3vCJuFgDuLeUXduycoZli"
)
func TestClarifai_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/clearbit/clearbit.go
================================================
package clearbit
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"clearbit"}) + `\b([0-9a-z_]{35})\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{"clearbit"}
}
// FromData will find and optionally verify Clearbit 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Clearbit,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://person.clearbit.com/v1/people/email/alex@alexmaccaw.com", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Clearbit
}
func (s Scanner) Description() string {
return "Clearbit provides powerful APIs for enriching data about companies and people. Clearbit API keys can be used to access and retrieve detailed information about these entities."
}
================================================
FILE: pkg/detectors/clearbit/clearbit_integration_test.go
================================================
//go:build detectors
// +build detectors
package clearbit
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestClearbit_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLEARBIT")
inactiveSecret := testSecrets.MustGetField("CLEARBIT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clearbit secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Clearbit,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clearbit secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Clearbit,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Clearbit.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Clearbit.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/clearbit/clearbit_test.go
================================================
package clearbit
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: Bearer
base_url: "https://api.example.com/v1/user"
clearbit_auth_token: "Bearer 9itvicgfiyq3ry6g03qwhwc_0s309dy8woh"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "9itvicgfiyq3ry6g03qwhwc_0s309dy8woh"
)
func TestClearBit_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/clickhelp/clickhelp.go
================================================
package clickhelp
import (
"context"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
portalPat = regexp.MustCompile(`\b([0-9A-Za-z-]{3,20}\.(?:try\.)?clickhelp\.co)\b`)
emailPat = regexp.MustCompile(common.EmailPattern)
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"clickhelp", "key", "token", "api", "secret"}) + `\b([0-9A-Za-z]{24})\b`)
)
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ClickHelp
}
func (s Scanner) Description() string {
return "ClickHelp is a documentation tool that allows users to create and manage online documentation. ClickHelp API keys can be used to access and modify documentation data."
}
// 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{"clickhelp.co"}
}
// FromData will find and optionally verify Clickhelp 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)
var uniquePortalLinks, uniqueEmails, uniqueAPIKeys = make(map[string]struct{}), make(map[string]struct{}), make(map[string]struct{})
for _, match := range portalPat.FindAllStringSubmatch(dataStr, -1) {
uniquePortalLinks[match[1]] = struct{}{}
}
for _, match := range emailPat.FindAllStringSubmatch(dataStr, -1) {
uniqueEmails[match[1]] = struct{}{}
}
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueAPIKeys[match[1]] = struct{}{}
}
for portalLink := range uniquePortalLinks {
for email := range uniqueEmails {
for apiKey := range uniqueAPIKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ClickHelp,
Raw: []byte(portalLink),
RawV2: []byte(portalLink + email),
}
if verify {
isVerified, verificationErr := verifyClickHelp(ctx, client, portalLink, email, apiKey)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
s1.SetPrimarySecretValue(apiKey) // line number will point to api key
}
results = append(results, s1)
}
}
}
return results, nil
}
func verifyClickHelp(ctx context.Context, client *http.Client, portalLink, email, apiKey string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s/api/v1/projects", portalLink), http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.SetBasicAuth(email, apiKey)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/clickhelp/clickhelp_integration_test.go
================================================
//go:build detectors
// +build detectors
package clickhelp
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestClickhelp_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
email := testSecrets.MustGetField("SCANNERS_EMAIL")
server := testSecrets.MustGetField("CLICKHELP_SERVER")
key := testSecrets.MustGetField("CLICKHELP")
inactiveKey := testSecrets.MustGetField("CLICKHELP_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clickhelp secret %s within clickhelp email %s and clickhelp server %s", key, email, server)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ClickHelp,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clickhelp secret %s within clickhelp email %s and clickhelp server %s but not valid", inactiveKey, email, server)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ClickHelp,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Clickhelp.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])
}
got[i].Raw = nil
got[i].RawV2 = nil
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "primarySecret")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AdafruitIO.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/clickhelp/clickhelp_test.go
================================================
package clickhelp
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
func TestClickHelp_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
user_email: "test-user@clickhelp.co"
key: "XzUkp562BtmjfRGoOGBiLLNu"
portal: testingdev.try.clickhelp.co
auth_type: Basic
base_url: "https://testing-dev.try.clickhelp.co/v1/user"
auth_token: ""
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`,
want: []string{
"testing-dev.try.clickhelp.cotest-user@clickhelp.co",
"testingdev.try.clickhelp.cotest-user@clickhelp.co",
},
},
{
name: "valid pattern - xml",
input: `
GLOBAL{test-user01@clickhelp.co}{AQAAABAAA XzUkp562BtmjfRGoOGBiLLNu}company-prod.clickhelp.coconfiguration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"company-prod.clickhelp.cotest-user01@clickhelp.co"},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/clicksendsms/clicksendsms.go
================================================
package clicksendsms
import (
"context"
b64 "encoding/base64"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(common.UUIDPatternUpperCase)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"sms"}) + common.EmailPattern)
)
// 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{"clicksendsms"}
}
// FromData will find and optionally verify ClickSendsms 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
resIdMatch := strings.TrimSpace(idMatch[0][strings.LastIndex(idMatch[0], " ")+1:])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ClickSendsms,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch),
}
if verify {
data := fmt.Sprintf("%s:%s", resIdMatch, resMatch)
sEnc := b64.StdEncoding.EncodeToString([]byte(data))
req, err := http.NewRequestWithContext(ctx, "GET", "https://rest.clicksend.com/v3/account", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ClickSendsms
}
func (s Scanner) Description() string {
return "ClickSend is a global leader in business communication solutions, providing a range of services including SMS, email, and voice. ClickSend API keys can be used to access and manage these communication services."
}
================================================
FILE: pkg/detectors/clicksendsms/clicksendsms_integration_test.go
================================================
//go:build detectors
// +build detectors
package clicksendsms
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestClickSendsms_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLICKSENDSMS_TOKEN")
inactiveSecret := testSecrets.MustGetField("CLICKSENDSMS_INACTIVE")
email := testSecrets.MustGetField("CLICKSENDSMS_EMAIL")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clicksendsms secret %s within clicksendsmsemail %s", secret, email)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ClickSendsms,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clicksendsms secret %s within clicksendsmsemail %s but not valid", inactiveSecret, email)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ClickSendsms,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("ClickSendsms.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])
}
got[i].Raw = nil
if len(got[i].RawV2) == 0 {
t.Fatalf("no raw v2 secret present: \n %+v", got[i])
}
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("ClickSendsms.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/clicksendsms/clicksendsms_test.go
================================================
package clicksendsms
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File for clicksendsms: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: Basic
base_url: "https://api.example.com/v1/sms"
sms_id: G9TXU2YD-NOYB-LLSX-21NU-5CX5SIA330Z7
sms_email: user-test@clicksend.sms
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "G9TXU2YD-NOYB-LLSX-21NU-5CX5SIA330Z7user-test@clicksend.sms"
)
func TestClickSendSMS_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/clickuppersonaltoken/clickuppersonaltoken.go
================================================
package clickuppersonaltoken
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"clickup"}) + `\b(pk_[0-9]{7,9}_[0-9A-Z]{32})\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{"clickup"}
}
// FromData will find and optionally verify ClickupPersonalToken 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[strings.TrimSpace(match[1])] = struct{}{}
}
for resMatch := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ClickupPersonalToken,
Raw: []byte(resMatch),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, err := verifyToken(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(err, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ClickupPersonalToken
}
func (s Scanner) Description() string {
return "ClickUp is a project management tool. Personal tokens can be used to access and modify data within ClickUp on behalf of a user."
}
func verifyToken(ctx context.Context, client *http.Client, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.clickup.com/api/v2/user", nil)
if err != nil {
return false, err
}
req.Header.Add("Authorization", 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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code %d", res.StatusCode)
}
}
================================================
FILE: pkg/detectors/clickuppersonaltoken/clickuppersonaltoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package clickuppersonaltoken
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestClickupPersonalToken_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLICKUPPERSONALTOKEN")
inactiveSecret := testSecrets.MustGetField("CLICKUPPERSONALTOKEN_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clickuppersonaltoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ClickupPersonalToken,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clickuppersonaltoken secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ClickupPersonalToken,
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 verifiable secret, verification failed due to unexpected API response",
s: Scanner{client: common.ConstantResponseHttpClient(404, "")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clickuppersonaltoken secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ClickupPersonalToken,
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("ClickupPersonalToken.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
got[i].Raw = nil
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("ClickupPersonalToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/clickuppersonaltoken/clickuppersonaltoken_test.go
================================================
package clickuppersonaltoken
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
base_url: "https://api.example.com/v1/user"
clickup_token: "pk_7043602_WIKY22PAKCVC1S5Q2X6119IK7N1UL8VY"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "pk_7043602_WIKY22PAKCVC1S5Q2X6119IK7N1UL8VY"
)
func TestClickupPersonalToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/cliengo/cliengo.go
================================================
package cliengo
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cliengo"}) + `\b([0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12})\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{"cliengo"}
}
// FromData will find and optionally verify Cliengo 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Cliengo,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cliengo.com/1.0/account?api_key="+resMatch, nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Cliengo
}
func (s Scanner) Description() string {
return "Cliengo is a chatbot service that helps businesses convert website visitors into leads. Cliengo API keys can be used to access and manage the chatbot configurations and data."
}
================================================
FILE: pkg/detectors/cliengo/cliengo_integration_test.go
================================================
//go:build detectors
// +build detectors
package cliengo
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCliengo_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLIENGO")
inactiveSecret := testSecrets.MustGetField("CLIENGO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cliengo secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cliengo,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cliengo secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cliengo,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Cliengo.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Cliengo.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/cliengo/cliengo_test.go
================================================
package cliengo
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
base_url: "https://api.cliengo.com/v1/user?key=9e4635bc-28dc-25d3-8546-2b30115d9a7b"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "9e4635bc-28dc-25d3-8546-2b30115d9a7b"
)
func TestCliengo_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/clientary/clientary.go
================================================
/*
RoninApp rebranded to Clientary
Article: https://www.clientary.com/articles/a-new-brand/
*/
package clientary
import (
"context"
"errors"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ronin", "clientary"}) + `\b([0-9a-zA-Z]{24,26})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ronin", "clientary"}) + `\b([0-9Aa-zA-Z-]{4,25})\b`)
errAccountNotFound = errors.New("account not found")
)
// 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{"ronin", "clientary"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Clientary
}
func (s Scanner) Description() string {
return "Clientary is a one software app to manage Clients, Invoices, Projects, Proposals, Estimates, Hours, Payments, Contractors and Staff. Clientary keys can be used to access and manage invoices and other resources."
}
// FromData will find and optionally verify RoninApp 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)
var uniqueIDs, uniqueAPIKeys = make(map[string]struct{}), make(map[string]struct{})
for _, match := range idPat.FindAllStringSubmatch(dataStr, -1) {
uniqueIDs[match[1]] = struct{}{}
}
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueAPIKeys[match[1]] = struct{}{}
}
for apiKey := range uniqueAPIKeys {
for id := range uniqueIDs {
// since regex matches can overlap, continue only if both apiKey and id are the same.
if apiKey == id {
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Clientary,
Raw: []byte(apiKey),
RawV2: []byte(apiKey + ":" + id),
ExtraData: make(map[string]string),
}
if verify {
isVerified, verificationErr := verifyClientaryAPIKey(ctx, client, id, apiKey)
s1.Verified = isVerified
if verificationErr != nil {
// remove the account ID if not found to prevent reuse during other API key checks.
if errors.Is(verificationErr, errAccountNotFound) {
delete(uniqueIDs, id)
continue
}
s1.SetVerificationError(verificationErr, apiKey)
}
// If a verified result is found, attach rebranding documentation to inform the user about the RoninApp rebranding to Clientary.
if s1.Verified {
s1.ExtraData["Rebrading Docs"] = "https://www.clientary.com/articles/a-new-brand/"
}
}
results = append(results, s1)
}
}
return results, nil
}
// docs: https://www.clientary.com/api
func verifyClientaryAPIKey(ctx context.Context, client *http.Client, id, apiKey string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://"+id+".clientary.com/api/v2/invoices", http.NoBody)
if err != nil {
return false, nil
}
req.SetBasicAuth(apiKey, apiKey)
req.Header.Add("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusForbidden, http.StatusUnauthorized:
return false, nil
case http.StatusNotFound:
// API return 404 if the account id does not exist
return false, errAccountNotFound
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/clientary/clientary_integration_test.go
================================================
//go:build detectors
// +build detectors
package clientary
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestRoninApp_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("RONINAPP")
inactiveSecret := testSecrets.MustGetField("RONINAPP_INACTIVE")
domain := testSecrets.MustGetField("RONINAPP_DOMAIN")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clientary secret %s and clientaryDomain %s", secret, domain)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Clientary,
Verified: false,
},
{
DetectorType: detectorspb.DetectorType_Clientary,
Verified: true,
ExtraData: map[string]string{
"Rebrading Docs": "https://www.clientary.com/articles/a-new-brand/",
},
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a ronin secret %s and ronaindomain %s but not valid", inactiveSecret, domain)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Clientary,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("RoninApp.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])
}
got[i].Raw = nil
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("RoninApp.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/clientary/clientary_test.go
================================================
package clientary
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
func TestRoninApp_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 ronin",
input: `
# some random code
data := getIDFromDatabase(ctx)
roninAPIKey := ZycQ0G6IBgNsBWytwzwVKixyz
roninDomain := truffle-dev.roninapp.com
`,
want: []string{"ZycQ0G6IBgNsBWytwzwVKixyz:truffle-dev"},
},
{
name: "valid pattern - with keyword clientary",
input: `
# some random code
data := getIDFromDatabase(ctx)
clientaryAPIKey := ZycQ0G6IBgNsBWytwzwVKixyz
clientaryDomain := truffle-dev.clientary.com
`,
want: []string{"ZycQ0G6IBgNsBWytwzwVKixyz:truffle-dev"},
},
{
name: "invalid pattern",
input: `
# some random code
data := getIDFromDatabase(ctx)
roninAPIKey := ZycQ0G6IBg-NsBWytwzwVKixyz
rominDomain := t_de.roninapp.com
`,
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)
}
})
}
}
================================================
FILE: pkg/detectors/clinchpad/clinchpad.go
================================================
package clinchpad
import (
"context"
b64 "encoding/base64"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"clinchpad"}) + `\b([a-z0-9]{32})\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{"clinchpad"}
}
// FromData will find and optionally verify Clinchpad 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Clinchpad,
Raw: []byte(resMatch),
}
if verify {
data := fmt.Sprintf("api-key:%s", resMatch)
sEnc := b64.StdEncoding.EncodeToString([]byte(data))
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.clinchpad.com/api/v1/pipelines", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Clinchpad
}
func (s Scanner) Description() string {
return "Clinchpad is a CRM tool. Clinchpad API keys can be used to access and modify data within Clinchpad."
}
================================================
FILE: pkg/detectors/clinchpad/clinchpad_integration_test.go
================================================
//go:build detectors
// +build detectors
package clinchpad
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestClinchpad_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLINCHPAD_TOKEN")
inactiveSecret := testSecrets.MustGetField("CLINCHPAD_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clinchpad secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Clinchpad,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clinchpad secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Clinchpad,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Clinchpad.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Clinchpad.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/clinchpad/clinchpad_test.go
================================================
package clinchpad
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Basic"
base_url: "https://api.example.com/v1/user"
clinchpad_key: "3v1xo5r03ghc538iwzbzeddwqulnun8h"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "3v1xo5r03ghc538iwzbzeddwqulnun8h"
)
func TestClinchPad_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/clockify/clockify.go
================================================
package clockify
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"clockify"}) + `\b([a-zA-Z0-9]{48})\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{"clockify"}
}
// FromData will find and optionally verify Clockify 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Clockify,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.clockify.me/api/v1/user", nil)
if err != nil {
continue
}
req.Header.Add("content-type", "application/json")
req.Header.Add("X-Api-Key", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Clockify
}
func (s Scanner) Description() string {
return "Clockify is a time tracking software. Clockify API keys can be used to access and modify time tracking data."
}
================================================
FILE: pkg/detectors/clockify/clockify_integration_test.go
================================================
//go:build detectors
// +build detectors
package clockify
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestClockify_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLOCKIFY")
inactiveSecret := testSecrets.MustGetField("CLOCKIFY_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clockify secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Clockify,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clockify secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Clockify,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Clockify.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Clockify.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/clockify/clockify_test.go
================================================
package clockify
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
base_url: "https://api.example.com/v1/user"
clockify_key: "kfJkRn7Knahh6pyDOL82NqNq5c4VLUNulVe5CMyJpIK9NXQC"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "kfJkRn7Knahh6pyDOL82NqNq5c4VLUNulVe5CMyJpIK9NXQC"
)
func TestClockify_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/clockworksms/clockworksms.go
================================================
package clockworksms
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
userKeyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"clockwork", "textanywhere"}) + `\b([0-9]{5})\b`)
tokenPat = regexp.MustCompile(detectors.PrefixRegex([]string{"clockwork", "textanywhere"}) + `\b([0-9a-zA-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{"clockworksms", "textanywhere"}
}
// FromData will find and optionally verify Clockworksms 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)
userKeyMatches := userKeyPat.FindAllStringSubmatch(dataStr, -1)
tokenMatches := tokenPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range userKeyMatches {
resMatch := strings.TrimSpace(match[1])
for _, tokenMatch := range tokenMatches {
tokenRes := strings.TrimSpace(tokenMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ClockworkSMS,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + tokenRes),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.textanywhere.com/API/v1.0/REST/status", nil)
if err != nil {
continue
}
req.Header.Add("user_key", resMatch)
req.Header.Add("access_token", tokenRes)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ClockworkSMS
}
func (s Scanner) Description() string {
return "Clockwork SMS is a service used for sending SMS messages. User keys and access tokens can be used to authenticate and send messages via the Clockwork SMS API."
}
================================================
FILE: pkg/detectors/clockworksms/clockworksms_integration_test.go
================================================
//go:build detectors
// +build detectors
package clockworksms
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestClockworksms_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
userKey := testSecrets.MustGetField("CLOCKWORKSMS_USERKEY")
token := testSecrets.MustGetField("CLOCKWORKSMS_TOKEN")
inactiveToken := testSecrets.MustGetField("CLOCKWORKSMS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clockworksms secret %s and clockworksms token %s within", userKey, token)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ClockworkSMS,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clockworksms secret %s and clockworksms token %s within but not valid", userKey, inactiveToken)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ClockworkSMS,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Clockworksms.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])
}
got[i].Raw = nil
if len(got[i].RawV2) == 0 {
t.Fatalf("no raw v2 secret present: \n %+v", got[i])
}
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Clockworksms.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/clockworksms/clockworksms_test.go
================================================
package clockworksms
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
base_url: "https://api.textanywhere.com/v1/user"
clockwork_key: "84473"
clockwork_token: "YROh7NZbZxHwiSc9pMNIAGYs"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "84473YROh7NZbZxHwiSc9pMNIAGYs"
)
func TestClockWorkSMS_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/closecrm/close.go
================================================
package closecrm
import (
"context"
b64 "encoding/base64"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(api_[a-z0-9A-Z.]{45})\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{"close"}
}
// FromData will find and optionally verify Close 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Close,
Raw: []byte(resMatch),
}
if verify {
data := fmt.Sprintf("%s:", resMatch)
sEnc := b64.StdEncoding.EncodeToString([]byte(data))
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.close.com/api/v1/me/", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Close
}
func (s Scanner) Description() string {
return "Close is a CRM software that helps businesses manage sales and customer relationships. Close API keys can be used to access and manipulate CRM data."
}
================================================
FILE: pkg/detectors/closecrm/close_integration_test.go
================================================
//go:build detectors
// +build detectors
package closecrm
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestClose_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLOSE_TOKEN")
inactiveSecret := testSecrets.MustGetField("CLOSE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a close secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Close,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a close secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Close,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Close.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Close.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/closecrm/close_test.go
================================================
package closecrm
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Basic"
base_url: "https://api.example.com/v1/user"
close_key: "api_3cyEW8syFEmeND561qJ9Sj8mT6E0VyWqY7h6cjJIBtc2e"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "api_3cyEW8syFEmeND561qJ9Sj8mT6E0VyWqY7h6cjJIBtc2e"
)
func TestCloseCRM_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/cloudconvert/cloudconvert.go
================================================
package cloudconvert
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.MaxSecretSizeProvider = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cloudconvert"}) + common.BuildRegexJWT("30,34", "200,500", "600,700"))
)
// 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{"cloudconvert"}
}
const maxJWTSize = 1300
// MaxSecretSize returns the maximum size of a secret that this detector can find.
func (s Scanner) MaxSecretSize() int64 { return maxJWTSize }
// FromData will find and optionally verify CloudConvert 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CloudConvert,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cloudconvert.com/v2/users/me", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/vnd.cloudconvert+json; version=3")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CloudConvert
}
func (s Scanner) Description() string {
return "CloudConvert is a file conversion service. CloudConvert API keys can be used to access and manage file conversion operations."
}
================================================
FILE: pkg/detectors/cloudconvert/cloudconvert_integration_test.go
================================================
//go:build detectors
// +build detectors
package cloudconvert
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCloudConvert_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLOUDCONVERT")
inactiveSecret := testSecrets.MustGetField("CLOUDCONVERT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudconvert secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CloudConvert,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudconvert secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CloudConvert,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CloudConvert.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CloudConvert.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/cloudconvert/cloudconvert_test.go
================================================
package cloudconvert
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
base_url: "https://api.example.com/v1/user"
cloudconvert_key: "eypn1gEV3BnckI3jcYzUvliSbukvxzO5acvE?ey8VEaR0lmpRa4IXv02fDPnlfdukWtb1/p-nlQlPVGnB52f9KwY4q98aVghXZqoit4AeFxMAHcCytOj61o8lHdcUF9fIcyF2HaFIk/k3Hdt7pS/5rb2eeWcEvc-5XB0T_Oh68AtCG8mOPpwKvzrhhIuEJck3vtFncgbDrSxg5mKkw924rMLP3Tb5tgIRuawZLwBxJL/qVIhAzfDGiIeNTzYOB9zHfHlfw3aZ1i/terePSN5EafVJ1yYw1KRXWL9/kPdAO0yFwSv3mUWx04oIIUURG6QKwO0rk7L0eAxnu4djnSXtqdvH_G50H1SSwwfKUg2Xz25-OZLkhxiaxEMMMY=3x0Yjhs7O1KFkI5gUQKH_VYAU2bJSAqpCKsxaYrdw91wUoya5rflCBVDHjC/BsezIkPFFmEu7sqs3WJg6dZeAiguYx7uZtDx1ILH18f29q9o34bM9SNolZNcG3fN7L2eWjilbmUq/Ty2545WkbHTjlcjLlHPAAjzLebfcFnlMSKH9Tqb/qx3G1z8wfzMa3dn3iRqNHwfmGOmfgK7RjtlZwoVruMjDWEza/o8imZF513yM7FrHTJkTFa1JjVbjU/C85ItZTiJsBUKAt/DbLg6W7lieKgHbgmz3cuwgVR7YDLZJB056TRcU9wrV0SUYDz0gogrpOEnZxdo4fb5UcCllj/AD/dYsfqVSHtTxKWBhun9Iqmx8FjgPtFCFugTxfaaHZ9dUC7TPahdSxixGvnu8EEvAs0Te85eJ9iyeq628Tvboz9J7KMq/uwflJtecSquJiWJT9GsYL5dl3Hr6ZYhxqs1-mrrB5FNzn-NPclPSu9PANtQ1BDuahKy683/t85F8yjug5C5paamNfgiJgOm5Vi/USUmWeVmH_htZoYGJTbOywDkRT1bYp9JIxlWHA29MInhWNrdlxZ_1h-SQ3fM6pzKIoJ0m_T/KXYERPzle0cy_/OnlfIa-yUgBnx_slQ1f9h0AS/PVMv/yZ6W"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "eypn1gEV3BnckI3jcYzUvliSbukvxzO5acvE?ey8VEaR0lmpRa4IXv02fDPnlfdukWtb1/p-nlQlPVGnB52f9KwY4q98aVghXZqoit4AeFxMAHcCytOj61o8lHdcUF9fIcyF2HaFIk/k3Hdt7pS/5rb2eeWcEvc-5XB0T_Oh68AtCG8mOPpwKvzrhhIuEJck3vtFncgbDrSxg5mKkw924rMLP3Tb5tgIRuawZLwBxJL/qVIhAzfDGiIeNTzYOB9zHfHlfw3aZ1i/terePSN5EafVJ1yYw1KRXWL9/kPdAO0yFwSv3mUWx04oIIUURG6QKwO0rk7L0eAxnu4djnSXtqdvH_G50H1SSwwfKUg2Xz25-OZLkhxiaxEMMMY=3x0Yjhs7O1KFkI5gUQKH_VYAU2bJSAqpCKsxaYrdw91wUoya5rflCBVDHjC/BsezIkPFFmEu7sqs3WJg6dZeAiguYx7uZtDx1ILH18f29q9o34bM9SNolZNcG3fN7L2eWjilbmUq/Ty2545WkbHTjlcjLlHPAAjzLebfcFnlMSKH9Tqb/qx3G1z8wfzMa3dn3iRqNHwfmGOmfgK7RjtlZwoVruMjDWEza/o8imZF513yM7FrHTJkTFa1JjVbjU/C85ItZTiJsBUKAt/DbLg6W7lieKgHbgmz3cuwgVR7YDLZJB056TRcU9wrV0SUYDz0gogrpOEnZxdo4fb5UcCllj/AD/dYsfqVSHtTxKWBhun9Iqmx8FjgPtFCFugTxfaaHZ9dUC7TPahdSxixGvnu8EEvAs0Te85eJ9iyeq628Tvboz9J7KMq/uwflJtecSquJiWJT9GsYL5dl3Hr6ZYhxqs1-mrrB5FNzn-NPclPSu9PANtQ1BDuahKy683/t85F8yjug5C5paamNfgiJgOm5Vi/USUmWeVmH_htZoYGJTbOywDkRT1bYp9JIxlWHA29MInhWNrdlxZ_1h-SQ3fM6pzKIoJ0m_T/KXYERPzle0cy_/OnlfIa-yUgBnx_slQ1f9h0AS/PVMv/yZ6W"
)
func TestCloudConvert_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/cloudelements/cloudelements.go
================================================
package cloudelements
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cloudelements"}) + `\b([a-zA-Z0-9]{43})\b`)
orgPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cloudelements"}) + `\b([a-z0-9]{32})\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{"cloudelements"}
}
// FromData will find and optionally verify CloudElements 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
orgMatches := orgPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, orgMatch := range orgMatches {
resOrgMatch := strings.TrimSpace(orgMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CloudElements,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resOrgMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://staging.cloud-elements.com/elements/api-v2/accounts", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("User %s, Organization %s", resMatch, resOrgMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CloudElements
}
func (s Scanner) Description() string {
return "CloudElements is an API integration platform that enables developers to connect their applications with various cloud services. CloudElements credentials can be used to access and manage these integrations."
}
================================================
FILE: pkg/detectors/cloudelements/cloudelements_integration_test.go
================================================
//go:build detectors
// +build detectors
package cloudelements
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCloudElements_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLOUDELEMENTS")
org := testSecrets.MustGetField("CLOUDELEMENTS_ORG")
inactiveSecret := testSecrets.MustGetField("CLOUDELEMENTS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudelements secret %s within cloudelements %s", secret, org)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CloudElements,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudelements secret %s within cloudelements %s but not valid", inactiveSecret, org)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CloudElements,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CloudElements.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CloudElements.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/cloudelements/cloudelements_test.go
================================================
package cloudelements
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
base_url: "https://api.example.com/v1/user"
cloudelements_key: "4NloL5EzH3PLvNzjCMikofUfKXYOsYOeJBopEyDScIL"
cloudelements_org: "inz9qofvjwnx59hgefq9sy5v64ilqrnu"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "4NloL5EzH3PLvNzjCMikofUfKXYOsYOeJBopEyDScILinz9qofvjwnx59hgefq9sy5v64ilqrnu"
)
func TestCloudElements_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/cloudflareapitoken/cloudflareapitoken.go
================================================
package cloudflareapitoken
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cloudflare"}) + `\b([A-Za-z0-9_-]{40})\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{"cloudflare"}
}
// FromData will find and optionally verify CloudflareApiToken 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CloudflareApiToken,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cloudflare.com/client/v4/user/tokens/verify", nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CloudflareApiToken
}
func (s Scanner) Description() string {
return "Cloudflare is a web infrastructure and website security company, providing content delivery network services, DDoS mitigation, Internet security, and distributed domain name server services. Cloudflare API tokens can be used to manage and interact with Cloudflare services."
}
================================================
FILE: pkg/detectors/cloudflareapitoken/cloudflareapitoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package cloudflareapitoken
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCloudflareApiToken_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLOUDFLARE_API_TOKEN")
inactiveSecret := testSecrets.MustGetField("CLOUDFLARE_API_INACTIVE")
secret2 := testSecrets.MustGetField("CLOUDFLARE_API_TOKEN2")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudflareapitoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CloudflareApiToken,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, verified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudflareapitoken secret %s within", secret2)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CloudflareApiToken,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudflareapitoken secret %s within but unverified", inactiveSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CloudflareApiToken,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CloudflareApiToken.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CloudflareApiToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/cloudflareapitoken/cloudflareapitoken_test.go
================================================
package cloudflareapitoken
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
base_url: "https://api.example.com/v1/user"
cloudflare_token: "kOjD1yceduu2jxL2uuwT9dkOIudU3_54sLCEud6j"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "kOjD1yceduu2jxL2uuwT9dkOIudU3_54sLCEud6j"
)
func TestCloudFlareAPIToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/cloudflarecakey/cloudflarecakey.go
================================================
package cloudflarecakey
import (
"context"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// origin ca keys documentation: https://developers.cloudflare.com/fundamentals/api/get-started/ca-keys/
keyPat = regexp.MustCompile(`\b(v1\.0-[A-Za-z0-9-]{171})\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{"cloudflare"}
}
// FromData will find and optionally verify CloudflareCaKey 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 _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueMatches[matches[1]] = struct{}{}
}
for caKey := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CloudflareCaKey,
Raw: []byte(caKey),
}
if verify {
isVerified, verificationErr := verifyCloudFlareCAKey(ctx, client, caKey)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, caKey)
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CloudflareCaKey
}
func (s Scanner) Description() string {
return "Cloudflare is a web infrastructure and website security company. Cloudflare CA keys can be used to manage SSL/TLS certificates and other security settings."
}
func verifyCloudFlareCAKey(ctx context.Context, client *http.Client, caKey string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cloudflare.com/client/v4/certificates?zone_id=a", nil)
if err != nil {
return false, nil
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("user-agent", "curl/7.68.0") // pretend to be from curl so we do not wait 100+ seconds -> nice try did not work
req.Header.Add("X-Auth-User-Service-Key", caKey)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/cloudflarecakey/cloudflarecakey_integration_test.go
================================================
//go:build detectors
// +build detectors
package cloudflarecakey
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCloudflareCaKey_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLOUDFLARE_ORIGIN_CA_KEY")
inactiveSecret := testSecrets.MustGetField("CLOUDFLARE_ORIGIN_CA_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
}{
{
name: "found, verified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudflarecakey secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CloudflareCaKey,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudflarecakey secret %s within but unverified", inactiveSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CloudflareCaKey,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CloudflareCaKey.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CloudflareCaKey.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/cloudflarecakey/cloudflarecakey_test.go
================================================
package cloudflarecakey
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
base_url: "https://api.cloudflare.com/v1/user"
ca_key: "v1.0-13vvv5141b975834504fc75b-a670d21e1e012816c3c8d9745e2693adc2d2ec7c402f607dbf7f2bd5de3bdb490cce4420ef13179957c5651e1ee5d952b1e03bd0271e2b43a9847f0713f4d3942cde4a7bc2e4770615"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "v1.0-13vvv5141b975834504fc75b-a670d21e1e012816c3c8d9745e2693adc2d2ec7c402f607dbf7f2bd5de3bdb490cce4420ef13179957c5651e1ee5d952b1e03bd0271e2b43a9847f0713f4d3942cde4a7bc2e4770615"
)
func TestCloudFlareCAKey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/cloudflareglobalapikey/cloudflareglobalapikey.go
================================================
package cloudflareglobalapikey
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
apiKeyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cloudflare"}) + `\b([A-Za-z0-9_-]{37})\b`)
emailPat = regexp.MustCompile(common.EmailPattern)
)
// 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{"cloudflare"}
}
// FromData will find and optionally verify CloudflareGlobalApiKey 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)
apiKeyMatches := apiKeyPat.FindAllStringSubmatch(dataStr, -1)
uniqueEmailMatches := make(map[string]struct{})
for _, match := range emailPat.FindAllStringSubmatch(dataStr, -1) {
uniqueEmailMatches[strings.TrimSpace(match[1])] = struct{}{}
}
for _, apiKeyMatch := range apiKeyMatches {
apiKeyRes := strings.TrimSpace(apiKeyMatch[1])
for emailMatch := range uniqueEmailMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CloudflareGlobalApiKey,
Redacted: emailMatch,
Raw: []byte(apiKeyRes),
RawV2: []byte(apiKeyRes + emailMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cloudflare.com/client/v4/user", nil)
if err != nil {
continue
}
req.Header.Add("X-Auth-Email", emailMatch)
req.Header.Add("X-Auth-Key", apiKeyRes)
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CloudflareGlobalApiKey
}
func (s Scanner) Description() string {
return "Cloudflare is a web infrastructure and website security company. Its services include content delivery network (CDN), DDoS mitigation, Internet security, and distributed domain name server (DNS) services. Cloudflare API keys can be used to access and modify these services."
}
================================================
FILE: pkg/detectors/cloudflareglobalapikey/cloudflareglobalapikey_integration_test.go
================================================
//go:build detectors
// +build detectors
package cloudflareglobalapikey
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCloudflareGlobalApiKey_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
globalApiKey := testSecrets.MustGetField("CLOUDFLARE_GLOBAL_API_KEY")
globalApiKeyEmail := testSecrets.MustGetField("CLOUDFLARE_GLOBAL_API_KEY_EMAIL")
inactiveglobalApiKey := testSecrets.MustGetField("CLOUDFLARE_GLOBAL_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
}{
{
name: "found, verified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudflare globalapikey secret %s within with email %s", globalApiKey, globalApiKeyEmail)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CloudflareGlobalApiKey,
Redacted: globalApiKeyEmail,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudflare globalapikey secret %s with email %s within but unverified", inactiveglobalApiKey, globalApiKeyEmail)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CloudflareGlobalApiKey,
Redacted: globalApiKeyEmail,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CloudflareGlobalApiKey.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CloudflareGlobalApiKey.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/cloudflareglobalapikey/cloudflareglobalapikey_test.go
================================================
package cloudflareglobalapikey
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 = "abcD123efg456HIJklmn789OPQ_rstUVWxYZ-012 / testuser1005@example.com"
invalidPattern = "abcD123efg456HIJklmn789OPQ_rstUVWxYZ-012/testing@go"
)
func TestCloudFlareGlobalAPIKey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: fmt.Sprintf("cloudflare: %s", validPattern),
want: []string{"abcD123efg456HIJklmn789OPQ_rstUVWxYZ-testuser1005@example.com"},
},
{
name: "valid pattern - key out of prefix range",
input: fmt.Sprintf("cloudflare keyword is not close to the real key and id = %s", validPattern),
want: nil,
},
{
name: "invalid pattern",
input: fmt.Sprintf("cloudflare: %s", invalidPattern),
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 && test.want != nil {
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) {
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)
}
})
}
}
================================================
FILE: pkg/detectors/cloudimage/cloudimage.go
================================================
package cloudimage
import (
"context"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cloudimage"}) + `\b([a-z0-9_]{30})\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{"cloudimage"}
}
// FromData will find and optionally verify CloudImage 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CloudImage,
Raw: []byte(resMatch),
}
if verify {
payload := strings.NewReader(`{"scope":"urls","urls":["/sample.li/paris.jpg?width=400","/sample.li/flat.jpg?width=400"]}
`)
timeout := 10 * time.Second
client.Timeout = timeout
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.cloudimage.com/invalidate", payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-Client-Key", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CloudImage
}
func (s Scanner) Description() string {
return "CloudImage is a service that provides image optimization and delivery. CloudImage API keys can be used to access and modify image data."
}
================================================
FILE: pkg/detectors/cloudimage/cloudimage_integration_test.go
================================================
//go:build detectors
// +build detectors
package cloudimage
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCloudImage_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLOUDIMAGE")
inactiveSecret := testSecrets.MustGetField("CLOUDIMAGE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudimage secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CloudImage,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudimage secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CloudImage,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CloudImage.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CloudImage.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/cloudimage/cloudimage_test.go
================================================
package cloudimage
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
base_url: "https://api.example.com/v1/user"
cloudimage: "d__9rvli8sm4jo18v5q0q4n7vhkwbv"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "d__9rvli8sm4jo18v5q0q4n7vhkwbv"
)
func TestCloudImage_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/cloudmersive/cloudmersive.go
================================================
package cloudmersive
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cloudmersive"}) + `\b([a-z0-9-]{36})\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{"cloudmersive"}
}
// FromData will find and optionally verify Cloudmersive 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Cloudmersive,
Raw: []byte(resMatch),
}
if verify {
payload := strings.NewReader(`{"AddressString":"string","CapitalizationMode":"string"}`)
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.cloudmersive.com/validate/address/parse", payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Apikey", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Cloudmersive
}
func (s Scanner) Description() string {
return "Cloudmersive provides a suite of APIs for data validation, conversion, and security. Cloudmersive API keys can be used to access these services."
}
================================================
FILE: pkg/detectors/cloudmersive/cloudmersive_integration_test.go
================================================
//go:build detectors
// +build detectors
package cloudmersive
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCloudmersive_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLOUDMERSIVE")
inactiveSecret := testSecrets.MustGetField("CLOUDMERSIVE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudmersive secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cloudmersive,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudmersive secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cloudmersive,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Cloudmersive.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Cloudmersive.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/cloudmersive/cloudmersive_test.go
================================================
package cloudmersive
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
base_url: "https://api.example.com/v1/user"
cloudmersive: "sxk5k1nfra8jak0mjjc6afr6v-6gsf7dr9o1"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "sxk5k1nfra8jak0mjjc6afr6v-6gsf7dr9o1"
)
func TestCloudMersive_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/cloudplan/cloudplan.go
================================================
package cloudplan
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cloudplan"}) + `\b([A-Z0-9-]{32})\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{"cloudplan"}
}
// FromData will find and optionally verify Cloudplan 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Cloudplan,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cloudplan.biz/api/user/me", nil)
if err != nil {
continue
}
req.Header.Add("session_id", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Cloudplan
}
func (s Scanner) Description() string {
return "Cloudplan is a service that offers cloud-based business solutions. Cloudplan session IDs can be used to access and manage user sessions and data."
}
================================================
FILE: pkg/detectors/cloudplan/cloudplan_integration_test.go
================================================
//go:build detectors
// +build detectors
package cloudplan
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCloudplan_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLOUDPLAN")
inactiveSecret := testSecrets.MustGetField("CLOUDPLAN_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudplan secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cloudplan,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudplan secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cloudplan,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Cloudplan.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
for i := range got {
if len(got[i].Raw) == 0 {
t.Fatal("no raw secret present")
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Cloudplan.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/cloudplan/cloudplan_test.go
================================================
package cloudplan
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
base_url: "https://api.example.com/v1/user"
cloudplan_session_key: "Y6D1FIS3XZXIJLKD82P6U8IXYV4UEYPP"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "Y6D1FIS3XZXIJLKD82P6U8IXYV4UEYPP"
)
func TestCloudPlan_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/cloudsmith/cloudsmith.go
================================================
package cloudsmith
import (
"context"
"encoding/json"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cloudsmith"}) + `\b([0-9a-f]{40})\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{"cloudsmith"}
}
type response struct {
Authenticated bool `json:"authenticated"`
}
// FromData will find and optionally verify Cloudsmith 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Cloudsmith,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cloudsmith.io/v1/user/self/", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/json")
req.Header.Add("X-Api-Key", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
var r response
if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
s1.SetVerificationError(err, resMatch)
continue
}
if r.Authenticated {
s1.Verified = true
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Cloudsmith
}
func (s Scanner) Description() string {
return "Cloudsmith is a cloud-native package management service. Cloudsmith API keys can be used to manage and distribute packages."
}
================================================
FILE: pkg/detectors/cloudsmith/cloudsmith_integration_test.go
================================================
//go:build detectors
// +build detectors
package cloudsmith
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCloudsmith_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLOUDSMITH")
inactiveSecret := testSecrets.MustGetField("CLOUDSMITH_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudsmith secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cloudsmith,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloudsmith secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cloudsmith,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Cloudsmith.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Cloudsmith.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/cloudsmith/cloudsmith_test.go
================================================
package cloudsmith
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "X-API-Key"
base_url: "https://api.example.com/v1/user"
cloudsmith: "6fd00a2cfd7bbc51e1c4db6ac2f29d59629afd22"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "6fd00a2cfd7bbc51e1c4db6ac2f29d59629afd22"
)
func TestCloudSmith_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/cloverly/cloverly.go
================================================
package cloverly
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cloverly"}) + `\b([a-z0-9:_]{28})\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{"cloverly"}
}
// FromData will find and optionally verify Cloverly 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Cloverly,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cloverly.com/2019-03-beta/account", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Cloverly
}
func (s Scanner) Description() string {
return "Cloverly is a platform that allows businesses to integrate carbon offsetting into their products and services. Cloverly API keys can be used to access and manage these offsetting services."
}
================================================
FILE: pkg/detectors/cloverly/cloverly_integration_test.go
================================================
//go:build detectors
// +build detectors
package cloverly
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCloverly_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLOVERLY")
inactiveSecret := testSecrets.MustGetField("CLOVERLY_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloverly secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cloverly,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloverly secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cloverly,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Cloverly.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Cloverly.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/cloverly/cloverly_test.go
================================================
package cloverly
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
base_url: "https://api.example.com/v1/user"
cloverly_token: "564i_0a9_v58bn:p9st3r3cgi95_"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "564i_0a9_v58bn:p9st3r3cgi95_"
)
func TestCloverly_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/cloze/cloze.go
================================================
package cloze
import (
"context"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cloze"}) + `\b([0-9a-f]{32})\b`)
emailPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cloze"}) + common.EmailPattern)
)
// 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{"cloze"}
}
// FromData will find and optionally verify Cloze 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
uniqueEmailMatches := make(map[string]struct{})
for _, match := range emailPat.FindAllStringSubmatch(dataStr, -1) {
uniqueEmailMatches[strings.TrimSpace(match[1])] = struct{}{}
}
for emailMatch := range uniqueEmailMatches {
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Cloze,
Raw: []byte(resMatch),
}
if verify {
payload := url.Values{}
payload.Add("user", emailMatch)
payload.Add("api_key", resMatch)
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cloze.com/v1/profile?"+payload.Encode(), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Cloze
}
func (s Scanner) Description() string {
return "Cloze is a relationship management tool that helps users manage their connections and interactions. Cloze API keys can be used to access and manage user data and interactions."
}
================================================
FILE: pkg/detectors/cloze/cloze_integration_test.go
================================================
//go:build detectors
// +build detectors
package cloze
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCloze_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
email := testSecrets.MustGetField("CLOZE_EMAIL")
secret := testSecrets.MustGetField("CLOZE")
inactiveSecret := testSecrets.MustGetField("CLOZE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloze user %s with cloze secret %s within", email, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cloze,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cloze user %s with cloze secret %s within but not valid", email, inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Cloze,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Cloze.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Cloze.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/cloze/cloze_test.go
================================================
package cloze
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 = "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d / testuser1005@example.com"
invalidPattern = "abcD123efg456HIJklmn789OPQ_rstUVWxYZ-012/testing@go"
)
func TestCloze_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: fmt.Sprintf("cloze: %s", validPattern),
want: []string{"1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"},
},
{
name: "valid pattern - key out of prefix range",
input: fmt.Sprintf("cloze keyword is not close to the real key and id = %s", validPattern),
want: nil,
},
{
name: "invalid pattern",
input: fmt.Sprintf("cloze: %s", invalidPattern),
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 && test.want != nil {
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) {
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)
}
})
}
}
================================================
FILE: pkg/detectors/clustdoc/clustdoc.go
================================================
package clustdoc
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"clustdoc"}) + `\b([0-9a-zA-Z]{60})\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{"clustdoc"}
}
// FromData will find and optionally verify ClustDoc 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ClustDoc,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://clustdoc.com/api/users", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ClustDoc
}
func (s Scanner) Description() string {
return "ClustDoc is a document management platform. ClustDoc API keys can be used to access and manage documents and workflows within the ClustDoc platform."
}
================================================
FILE: pkg/detectors/clustdoc/clustdoc_integration_test.go
================================================
//go:build detectors
// +build detectors
package clustdoc
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestClustDoc_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CLUSTDOC")
inactiveSecret := testSecrets.MustGetField("CLUSTDOC_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clustdoc secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ClustDoc,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a clustdoc secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ClustDoc,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("ClustDoc.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("ClustDoc.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/clustdoc/clustdoc_test.go
================================================
package clustdoc
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
base_url: "https://api.example.com/v1/user"
clustdoc_token: "yQ7mTTO4eJ4I9GHDEdzF3wq0KVowNKjPMud3q0ZqaEIuoR1qCrARUyLwknNP"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "yQ7mTTO4eJ4I9GHDEdzF3wq0KVowNKjPMud3q0ZqaEIuoR1qCrARUyLwknNP"
)
func TestClustDoc_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/coda/coda.go
================================================
package coda
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"coda"}) + `\b([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})\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{"coda"}
}
// FromData will find and optionally verify Coda 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Coda,
Raw: []byte(resMatch),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
req, err := http.NewRequestWithContext(ctx, "GET", "https://coda.io/apis/v1/whoami", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
} else if res.StatusCode == 401 {
// The secret is determinately not verified (nothing to do)
} else {
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
s1.SetVerificationError(err, resMatch)
}
} else {
s1.SetVerificationError(err, resMatch)
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Coda
}
func (s Scanner) Description() string {
return "Coda is a platform for building collaborative documents and applications. Coda API keys can be used to access and manipulate data within Coda documents and applications."
}
================================================
FILE: pkg/detectors/coda/coda_integration_test.go
================================================
//go:build detectors
// +build detectors
package coda
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/detectorspb"
)
func TestCoda_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("CODA")
inactiveSecret := testSecrets.MustGetField("CODA_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a coda secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Coda,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a coda secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Coda,
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 coda secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Coda,
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 coda secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Coda,
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("Coda.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Coda.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/coda/coda_test.go
================================================
package coda
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
base_url: "https://api.example.com/v1/user"
coda_token: "64ukni4l-zub4-3coe-html-9byb40oi5i87"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "64ukni4l-zub4-3coe-html-9byb40oi5i87"
)
func TestCoda_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/codacy/codacy.go
================================================
package codacy
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"codacy"}) + `\b([0-9A-Za-z]{20})\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{"codacy"}
}
// FromData will find and optionally verify Codacy 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Codacy,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://app.codacy.com/api/v3/user", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/json")
req.Header.Add("api-token", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Codacy
}
func (s Scanner) Description() string {
return "Codacy is an automated code review tool that helps developers and teams improve code quality. Codacy API tokens can be used to access and manage code quality reports and settings."
}
================================================
FILE: pkg/detectors/codacy/codacy_integration_test.go
================================================
//go:build detectors
// +build detectors
package codacy
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCodacy_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CODACY")
inactiveSecret := testSecrets.MustGetField("CODACY_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a codacy secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Codacy,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a codacy secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Codacy,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Codacy.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Codacy.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/codacy/codacy_test.go
================================================
package codacy
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
base_url: "https://api.example.com/v1/user"
codacy_token: "g73RSmTIzTU1wUA5BXYI"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "g73RSmTIzTU1wUA5BXYI"
)
func TestCodacy_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/codeclimate/codeclimate.go
================================================
package codeclimate
import (
"context"
"encoding/json"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"codeclimate"}) + `\b([a-f0-9]{40})\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{"codeclimate"}
}
type response struct {
Data struct {
Id string `json:"id"`
} `json:"data"`
}
// FromData will find and optionally verify Codeclimate 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Codeclimate,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.codeclimate.com/v1/user", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/vnd.api+json")
req.Header.Add("Authorization", fmt.Sprintf("Token token=%s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
var r response
if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
s1.SetVerificationError(err, resMatch)
continue
}
if r.Data.Id != "" {
s1.Verified = true
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Codeclimate
}
func (s Scanner) Description() string {
return "Codeclimate is a tool for automated code review and analysis. Codeclimate tokens can be used to access and manage repositories and their analysis results."
}
================================================
FILE: pkg/detectors/codeclimate/codeclimate_integration_test.go
================================================
//go:build detectors
// +build detectors
package codeclimate
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCodeclimate_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CODECLIMATE")
inactiveSecret := testSecrets.MustGetField("CODECLIMATE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a codeclimate secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Codeclimate,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a codeclimate secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Codeclimate,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Codeclimate.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Codeclimate.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/codeclimate/codeclimate_test.go
================================================
package codeclimate
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
base_url: "https://api.example.com/v1/user"
codeclimate_token: "efbc069555c703d31c3bcc6fbd426cec5f21eb43"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "efbc069555c703d31c3bcc6fbd426cec5f21eb43"
)
func TestCodeClimate_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/codemagic/codemagic.go
================================================
package codemagic
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"codemagic"}) + common.BuildRegex(common.AlphaNumPattern, "_", 43))
)
// 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{"codemagic"}
}
// FromData will find and optionally verify Codemagic 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Codemagic,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.codemagic.io/apps", nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("x-auth-token", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Codemagic
}
func (s Scanner) Description() string {
return "Codemagic is a CI/CD platform for mobile app projects. Codemagic API keys can be used to automate and manage the build and deployment process of mobile applications."
}
================================================
FILE: pkg/detectors/codemagic/codemagic_integration_test.go
================================================
//go:build detectors
// +build detectors
package codemagic
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCodemagic_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CODEMAGIC")
inactiveSecret := testSecrets.MustGetField("CODEMAGIC_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a codemagic secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Codemagic,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a codemagic secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Codemagic,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Codemagic.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Codemagic.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/codemagic/codemagic_test.go
================================================
package codemagic
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Auth-Key"
base_url: "https://api.example.com/v1/user"
codemagic_key: "PSIYbVgfkbEPQoqJfzHpACTtihONkQ_cKmOpNDPNiCU"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "PSIYbVgfkbEPQoqJfzHpACTtihONkQ_cKmOpNDPNiCU"
)
func TestCodeMagic_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/codequiry/codequiry.go
================================================
package codequiry
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"codequiry"}) + `\b([a-zA-Z-0-9]{64})\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{"codequiry"}
}
// FromData will find and optionally verify Codequiry 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Codequiry,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://codequiry.com/api/v1/checks", nil)
if err != nil {
continue
}
req.Header.Add("apikey", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Codequiry
}
func (s Scanner) Description() string {
return "Codequiry is a plagiarism detection service. Codequiry API keys can be used to access and utilize their plagiarism detection features."
}
================================================
FILE: pkg/detectors/codequiry/codequiry_integration_test.go
================================================
//go:build detectors
// +build detectors
package codequiry
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCodequiry_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CODEQUIRY")
inactiveSecret := testSecrets.MustGetField("CODEQUIRY_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a codequiry secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Codequiry,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a codequiry secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Codequiry,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Codequiry.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Codequiry.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/codequiry/codequiry_test.go
================================================
package codequiry
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
base_url: "https://api.example.com/v1/user"
codequiry_key: "7cA6eb3AvmlVSqLMP4iKvp7fXtEAADQud11KidjPzbSLcvntAD8CK6P7uGaGdlit"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "7cA6eb3AvmlVSqLMP4iKvp7fXtEAADQud11KidjPzbSLcvntAD8CK6P7uGaGdlit"
)
func TestCodeQuiry_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/coinapi/coinapi.go
================================================
package coinapi
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"coinapi"}) + `\b([A-Z0-9-]{36})\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{"coinapi"}
}
// FromData will find and optionally verify CoinApi 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CoinApi,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://rest.coinapi.io/v1/exchanges", nil)
if err != nil {
continue
}
req.Header.Add("X-CoinAPI-Key", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CoinApi
}
func (s Scanner) Description() string {
return "CoinApi provides a RESTful API to access cryptocurrency market data. CoinApi keys can be used to fetch real-time and historical cryptocurrency data."
}
================================================
FILE: pkg/detectors/coinapi/coinapi_integration_test.go
================================================
//go:build detectors
// +build detectors
package coinapi
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCoinApi_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("COINAPI")
inactiveSecret := testSecrets.MustGetField("COINAPI_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a coinapi secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CoinApi,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a coinapi secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CoinApi,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CoinApi.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CoinApi.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/coinapi/coinapi_test.go
================================================
package coinapi
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
base_url: "https://api.example.com/v1/user"
coinapi_key: "6D8B5AUIRDQCB3NRKSMZZL9RCV9G07GTHUR3"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "6D8B5AUIRDQCB3NRKSMZZL9RCV9G07GTHUR3"
)
func TestCoinAPI_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/coinbase/coinbase.go
================================================
package coinbase
import (
"context"
"crypto/ecdsa"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
"github.com/golang-jwt/jwt/v5"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Reference: https://docs.cdp.coinbase.com/coinbase-app/docs/auth/api-key-authentication
keyNamePat = regexp.MustCompile(`\b(organizations\\*/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\\*/apiKeys\\*/\w{8}-\w{4}-\w{4}-\w{4}-\w{12})\b`)
privateKeyPat = regexp.MustCompile(`(-----BEGIN EC(?:DSA)? PRIVATE KEY-----(?:\r|\n|\\+r|\\+n)(?:[a-zA-Z0-9+/]+={0,2}(?:\r|\n|\\+r|\\+n))+-----END EC(?:DSA)? PRIVATE KEY-----(?:\r|\n|\\+r|\\+n)?)`)
apiHost = "api.coinbase.com"
verificationEndpoint = "/v2/user"
verificationMethod = http.MethodGet
verificationURI = fmt.Sprintf("https://%s%s", apiHost, verificationEndpoint)
nameReplacer = strings.NewReplacer("\\", "")
keyReplacer = strings.NewReplacer(
"\r\n", "\n",
"\\r\\n", "\n",
"\\n", "\n",
"\\r", "\n",
)
)
// 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{"begin ec"}
}
func isValidECPrivateKey(pemKey []byte) bool {
block, _ := pem.Decode(pemKey)
if block == nil {
return false
}
key, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return false
}
// Check the key type
_, ok := key.Public().(*ecdsa.PublicKey)
return ok
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Coinbase 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)
uniqueKeyNames, uniquePrivateKeys := map[string]struct{}{}, map[string]struct{}{}
for _, keyNameMatch := range keyNamePat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeyNames[keyNameMatch[1]] = struct{}{}
}
for _, privateKeyMatch := range privateKeyPat.FindAllStringSubmatch(dataStr, -1) {
uniquePrivateKeys[privateKeyMatch[1]] = struct{}{}
}
for keyName := range uniqueKeyNames {
for privateKey := range uniquePrivateKeys {
client := s.getClient()
resKeyName := nameReplacer.Replace(strings.TrimSpace(keyName))
resPrivateKey := keyReplacer.Replace(strings.TrimSpace(privateKey))
if !isValidECPrivateKey([]byte(resPrivateKey)) {
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Coinbase,
Raw: []byte(resPrivateKey),
RawV2: []byte(fmt.Sprintf("%s:%s", resKeyName, resPrivateKey)),
}
if verify {
isVerified, verificationErr := s.verifyMatch(ctx, client, resKeyName, resPrivateKey)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resPrivateKey)
}
results = append(results, s1)
// If we've found a verified match with this ID, we don't need to look for anymore. So move on to the next ID.
if s1.Verified {
break
}
}
}
return results, nil
}
func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, keyName, privateKey string) (bool, error) {
jwtToken, err := buildJWT(verificationMethod, apiHost, verificationEndpoint, keyName, privateKey)
if err != nil {
return false, err
}
req, err := http.NewRequestWithContext(ctx, verificationMethod, verificationURI, http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", jwtToken))
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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code %d", res.StatusCode)
}
}
// Coinbase API requires the credentials encoded in a JWT token
// The JWT token is signed with the private key and expires in 2 minutes
func buildJWT(method, host, endpoint, keyName, key string) (string, error) {
// Decode the PEM key
pemStr := strings.ReplaceAll(key, `\n`, "\n")
block, _ := pem.Decode([]byte(pemStr))
if block == nil || block.Type != "EC PRIVATE KEY" {
return "", fmt.Errorf("failed to decode PEM block containing EC private key")
}
privateKey, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return "", fmt.Errorf("failed to parse EC private key: %v", err)
}
now := time.Now().Unix()
claims := jwt.MapClaims{
"sub": keyName,
"iss": "cdp",
"nbf": now,
"exp": now + 120,
"uri": fmt.Sprintf("%s %s%s", method, host, endpoint),
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
token.Header["kid"] = keyName
token.Header["nonce"] = fmt.Sprintf("%x", makeNonce())
signedToken, err := token.SignedString(privateKey)
if err != nil {
return "", fmt.Errorf("failed to sign JWT: %v", err)
}
return signedToken, nil
}
func makeNonce() []byte {
nonce := make([]byte, 16) // 128-bit nonce
_, _ = rand.Read(nonce)
return nonce
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Coinbase
}
func (s Scanner) Description() string {
return "Coinbase is a digital currency exchange that allows users to buy, sell, and store various cryptocurrencies. A Coinbase API key name and private key can be used to access and manage a user's account and transactions."
}
================================================
FILE: pkg/detectors/coinbase/coinbase_integration_test.go
================================================
//go:build detectors
// +build detectors
package coinbase
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/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCoinbase_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)
}
keyName := testSecrets.MustGetField("COINBASE_KEY_NAME")
privateKey := testSecrets.MustGetField("COINBASE_PRIVATE_KEY")
inactiveKeyName := testSecrets.MustGetField("COINBASE_INACTIVE_KEY_NAME")
inactivePrivateKey := testSecrets.MustGetField("COINBASE_INACTIVE_PRIVATE_KEY")
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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a coinbase secret %s %s within", keyName, privateKey)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Coinbase,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a coinbase secret %s %s within but not valid", inactiveKeyName, inactivePrivateKey)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Coinbase,
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 coinbase secret %s %s within", keyName, privateKey)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Coinbase,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: true,
},
{
name: "found, verified but unexpected api surface",
s: Scanner{client: common.ConstantResponseHttpClient(http.StatusInternalServerError, "")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a coinbase secret %s %s within", keyName, privateKey)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Coinbase,
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("Coinbase.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "primarySecret")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Coinbase.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/coinbase/coinbase_test.go
================================================
package coinbase
import (
"context"
"testing"
)
func TestCoinbase_Pattern(t *testing.T) {
tests := []struct {
name string
data string
shouldMatch bool
match string
}{
// True positives
// https://github.com/coinbase/waas-client-library-go/issues/41
{
name: "valid_result1",
data: `{ "name": "organizations/14d1742b-3575-4490-b9bc-a8a9c7e4973d/apiKeys/7473d38c-80c6-4a69-a715-1ea8fd950f6f", "principal": "8feb538e-137b-5864-b12a-7c75b60fa20a", "principalType": "USER", "publicKey": "-----BEGIN EC PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzR0G+CW0uVJFrpLUELqB+DlsmGmO\nA03Az8Fpv7azpgjAy87ibgQTThaQy1C1BccbCDkPoEs6mOnDkOebkybAKQ==\n-----END EC PUBLIC KEY-----\n", "privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBddyynZ9Ya7op1B9nu1Dxyc1T6xLy72t45J2Smv9oXNoAoGCCqGSM49\nAwEHoUQDQgAEzR0G+CW0uVJFrpLUELqB+DlsmGmOA03Az8Fpv7azpgjAy87ibgQT\nThaQy1C1BccbCDkPoEs6mOnDkOebkybAKQ==\n-----END EC PRIVATE KEY-----\n", "createTime": "2023-08-19T12:29:08.938421763Z", "projectId": "5970e137-9c3d-4adc-b65d-58d33af2432d" }`,
shouldMatch: true,
match: "organizations/14d1742b-3575-4490-b9bc-a8a9c7e4973d/apiKeys/7473d38c-80c6-4a69-a715-1ea8fd950f6f:-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBddyynZ9Ya7op1B9nu1Dxyc1T6xLy72t45J2Smv9oXNoAoGCCqGSM49\nAwEHoUQDQgAEzR0G+CW0uVJFrpLUELqB+DlsmGmOA03Az8Fpv7azpgjAy87ibgQT\nThaQy1C1BccbCDkPoEs6mOnDkOebkybAKQ==\n-----END EC PRIVATE KEY-----\n",
},
// https://github.com/coinbase/waas-client-library-go/pull/32#issuecomment-1666415017
{
name: "valid_result2_name_slashes",
data: `{
"name": "organizations\/d3f266dc-0d36-4cd0-91c3-e3a292b0b4b3\/apiKeys\/032c4fdf-d763-4b0c-9ed3-ff41a873bcc8",
"principal": "5d5c9f00-3224-52a7-a1f7-9e6ce3ada40c",
"principalType": "USER",
"publicKey": "-----BEGIN EC PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAjw43hwOqS2PF4gAFbhoxIJqCHAP\niqLdg5GFVn9QAS/0oY4/fJGrCn9rpQGOvHxHf1mtQ6j4bIWN1AtHvA/3uw==\n-----END EC PUBLIC KEY-----\n",
"privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIFkA1kU4DlNu36wTTHycWy6n1rsUH0UT8mfAKNtOukXHoAoGCCqGSM49\nAwEHoUQDQgAEAjw43hwOqS2PF4gAFbhoxIJqCHAPiqLdg5GFVn9QAS/0oY4/fJGr\nCn9rpQGOvHxHf1mtQ6j4bIWN1AtHvA/3uw==\n-----END EC PRIVATE KEY-----\n",
"createTime": "2023-08-05T06:34:40.265235553Z",
"projectId": "64b3f391-c69d-4c59-91a2-75816c1a0738"
}`,
shouldMatch: true,
match: "organizations/d3f266dc-0d36-4cd0-91c3-e3a292b0b4b3/apiKeys/032c4fdf-d763-4b0c-9ed3-ff41a873bcc8:-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIFkA1kU4DlNu36wTTHycWy6n1rsUH0UT8mfAKNtOukXHoAoGCCqGSM49\nAwEHoUQDQgAEAjw43hwOqS2PF4gAFbhoxIJqCHAPiqLdg5GFVn9QAS/0oY4/fJGr\nCn9rpQGOvHxHf1mtQ6j4bIWN1AtHvA/3uw==\n-----END EC PRIVATE KEY-----\n",
},
{
name: "valid_result3",
data: `name: "organizations/7eead2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9",
description: "principal": "775fb863-004f-5412-8e4c-e9449c612563" and install dependencies
runs: "principalType": "USER",
using: composite
steps:"publicKey": "-----BEGIN EC PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvHsvI08kox+n/8wSMFwCbK5hEf5b\n/g82Lmz3HpATKFmrICcOBX2lRHo99JWRrupmjUGxnD8i4sj4mZafTEokhA==\n-----END EC PUBLIC KEY-----\n",
- name: Setup Node.js
uses: actions/setup-node@v3
with: "privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIKOQ7lvGL0EiUzZ23pmH/NBPRwVV8yZsqofds5bSR9qFoAoGCCqGSM49\nAwEHoUQDQgAEvHsvI08kox+n/8wSMFwCbK5hEf5b/g82Lmz3HpATKFmrICcOBX2l\nRHo99JWRrupmjUGxnD8i4sj4mZafTEokhA==\n-----END EC PRIVATE KEY-----\n",
node-version-file: .nvmrc
- name: Cache dependencies`,
shouldMatch: true,
match: "organizations/7eead2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9:-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIKOQ7lvGL0EiUzZ23pmH/NBPRwVV8yZsqofds5bSR9qFoAoGCCqGSM49\nAwEHoUQDQgAEvHsvI08kox+n/8wSMFwCbK5hEf5b/g82Lmz3HpATKFmrICcOBX2l\nRHo99JWRrupmjUGxnD8i4sj4mZafTEokhA==\n-----END EC PRIVATE KEY-----\n",
},
{
name: "valid_result_ecdsa",
data: `{
"name": "organizations/7eead2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9",
"privateKey": "-----BEGIN ECDSA PRIVATE KEY-----\nMHcCAQEEINQdZMbF2r07KF0mxfLYt9Y1PNaC0C6UpZ31MxD4NEE8oAoGCCqGSM49\nAwEHoUQDQgAEeRFgMrQEHI/APWaziRH90jN7EozjdbPVxvzc1F4zqWTeCtLASwqA\nqnMugYX2epqsFhGn82xNXu2NwgORc6embQ==\n-----END ECDSA PRIVATE KEY-----\n"
}`,
shouldMatch: true,
match: "organizations/7eead2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9:-----BEGIN ECDSA PRIVATE KEY-----\nMHcCAQEEINQdZMbF2r07KF0mxfLYt9Y1PNaC0C6UpZ31MxD4NEE8oAoGCCqGSM49\nAwEHoUQDQgAEeRFgMrQEHI/APWaziRH90jN7EozjdbPVxvzc1F4zqWTeCtLASwqA\nqnMugYX2epqsFhGn82xNXu2NwgORc6embQ==\n-----END ECDSA PRIVATE KEY-----\n",
},
// TODO: Is it worth supporting case-insensitive headers?
// https://github.com/coinbase/waas-sdk-react-native/blob/bbaf597e73d02ecaf64161061e71b85d9eeeb9d6/example/src/.coinbase_cloud_api_key.json#L4
// {
// name: "valid_result_case_insensitive",
// data: `{
// "name": "organizations/7eead2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9",
// "privateKey": "-----BEGIN ECDSA private key-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8id7yCfmNp0ppczu\nDhjB1pesdDB6Uwuz6KxARrenNfyhRANCAASI6DBntdr+XSOaK55J++x8ORuDxn81\nENa0RmGFjTwu4vQcWcx5rrIWNh6b7FPxy6mrZl0n3rswEtZmUci8Y5HX\n-----END ECDSA PRIVATE KEY-----\n"
//}`,
// shouldMatch: true,
// match: "organizations/7eegad2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9:-----BEGIN ECDSA PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8id7yCfmNp0ppczu\nDhjB1pesdDB6Uwuz6KxARrenNfyhRANCAASI6DBntdr+XSOaK55J++x8ORuDxn81\nENa0RmGFjTwu4vQcWcx5rrIWNh6b7FPxy6mrZl0n3rswEtZmUci8Y5HX\n-----END ECDSA PRIVATE KEY-----\n",
// },
// False positives
// https://github.com/coinbase/waas-client-library-go/blob/main/example.go
{
name: `invalid_key_name1`,
data: `const (
// apiKeyName is the name of the API Key to use. Fill this out before running the main function.
apiKeyName = "organizations/my-organization/apiKeys/my-api-key"
// privKeyTemplate is the private key of the API Key to use. Fill this out before running the main function.
privKeyTemplate = "-----BEGIN EC PRIVATE KEY-----\nmy-private-key\n-----END EC PRIVATE KEY-----\n"
)`,
shouldMatch: false,
},
// https://github.com/coinbase/waas-sdk-react-native/blob/bbaf597e73d02ecaf64161061e71b85d9eeeb9d6/example/src/.coinbase_cloud_api_key.json#L4
{
name: `invalid_key_name2`,
data: `{
"name": "organizations/organizationID/apiKeys/apiKeyName",
"privateKey": "-----BEGIN ECDSA Private Key-----ExamplePrivateKey-----END ECDSA Private Key-----\n"
}`,
},
{
name: `invalid_private_key`,
data: `{ "name": "organizations/14d1742b-3575-4490-b9bc-a8a9c7e4973d/apiKeys/7473d38c-80c6-4a69-a715-1ea8fd950f6f", "principal": "8feb538e-137b-5864-b12a-7c75b60fa20a", "principalType": "USER", "publicKey": "-----BEGIN EC PUBLIC KEY-----\ninvalid\n-----END EC PUBLIC KEY-----\n", "privateKey": "-----BEGIN EC PRIVATE KEY-----\ninvalid\n-----END EC PRIVATE KEY-----\n", "createTime": "2023-08-19T12:29:08.938421763Z", "projectId": "5970e137-9c3d-4adc-b65d-58d33af2432d" }`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := Scanner{}
results, err := s.FromData(context.Background(), false, []byte(test.data))
if err != nil {
t.Errorf("Coinbase.FromData() error = %v", err)
return
}
if test.shouldMatch {
if len(results) == 0 {
t.Errorf("%s: did not receive a match for '%v' when one was expected", test.name, test.data)
return
}
expected := test.data
if test.match != "" {
expected = test.match
}
result := results[0]
resultData := string(result.RawV2)
if resultData != expected {
t.Errorf("%s: did not receive expected match.\n\texpected: '%s'\n\t actual: '%s'", test.name, expected, resultData)
return
}
} else {
if len(results) > 0 {
t.Errorf("%s: received a match for '%v' when one wasn't wanted", test.name, test.data)
return
}
}
})
}
}
================================================
FILE: pkg/detectors/coinlayer/coinlayer.go
================================================
package coinlayer
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"coinlayer"}) + `\b([a-z0-9]{32})\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{"coinlayer"}
}
// FromData will find and optionally verify Coinlayer 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Coinlayer,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.coinlayer.com/api/livelive?access_key=%s", resMatch), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
bodyBytes, err := io.ReadAll(res.Body)
if err == nil {
bodyString := string(bodyBytes)
validResponse := strings.Contains(bodyString, `"success": true`) || strings.Contains(bodyString, `"info":"Access Restricted - Your current Subscription Plan does not support HTTPS Encryption."`)
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
if validResponse {
s1.Verified = true
} else {
s1.Verified = false
}
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Coinlayer
}
func (s Scanner) Description() string {
return "Coinlayer provides real-time and historical cryptocurrency exchange rates. The API key can be used to access this data."
}
================================================
FILE: pkg/detectors/coinlayer/coinlayer_integration_test.go
================================================
//go:build detectors
// +build detectors
package coinlayer
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCoinlayer_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("COINLAYER")
inactiveSecret := testSecrets.MustGetField("COINLAYER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a coinlayer secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Coinlayer,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a coinlayer secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Coinlayer,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Coinlayer.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Coinlayer.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/coinlayer/coinlayer_test.go
================================================
package coinlayer
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
base_url: "https://api.coinlayer.com/v1/user?key=gg2einqoe3zxu0ju7c3wqg9vql662vdj"
key: ""
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "gg2einqoe3zxu0ju7c3wqg9vql662vdj"
)
func TestCoinLayer_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/coinlib/coinlib.go
================================================
package coinlib
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"coinlib"}) + `\b([a-z0-9]{16})\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{"coinlib"}
}
// FromData will find and optionally verify Coinlib 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Coinlib,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://coinlib.io/api/v1/global?key=%s&pref=EUR", resMatch), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Coinlib
}
func (s Scanner) Description() string {
return "Coinlib is a cryptocurrency data provider. Coinlib API keys can be used to access and retrieve cryptocurrency data."
}
================================================
FILE: pkg/detectors/coinlib/coinlib_integration_test.go
================================================
//go:build detectors
// +build detectors
package coinlib
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCoinlib_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("COINLIB")
inactiveSecret := testSecrets.MustGetField("COINLIB_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a coinlib secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Coinlib,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a coinlib secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Coinlib,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Coinlib.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Coinlib.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/coinlib/coinlib_test.go
================================================
package coinlib
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
base_url: "https://api.coinlib.com/v1/user?key=seugeupfknprstoe"
key: ""
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "seugeupfknprstoe"
)
func TestCoinLib_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/collect2/collect2.go
================================================
package collect2
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"collect2"}) + `\b([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\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{"collect2"}
}
// FromData will find and optionally verify Collect2 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Collect2,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://collect2.com/api/%s/datarecord/", resMatch), nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Collect2
}
func (s Scanner) Description() string {
return "An API to Collect, Modify, Filter and Export Data using webhooks. API keys can create read update and delete data."
}
================================================
FILE: pkg/detectors/collect2/collect2_integration_test.go
================================================
//go:build detectors
// +build detectors
package collect2
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCollect2_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("COLLECT2")
inactiveSecret := testSecrets.MustGetField("COLLECT2_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a collect2 secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Collect2,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a collect2 secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Collect2,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Collect2.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Collect2.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/collect2/collect2_test.go
================================================
package collect2
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
base_url: "https://api.collect2.com/v1/user?key=22f39f53-3bd4-d84b-8e29-00402d5c316f"
key: ""
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "22f39f53-3bd4-d84b-8e29-00402d5c316f"
)
func TestCollect2_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/column/column.go
================================================
package column
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"column"}) + `\b((?:test|live)_[a-zA-Z0-9]{27})\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{"column"}
}
// FromData will find and optionally verify Column 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Column,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.column.com/entities", nil)
if err != nil {
continue
}
// req.SetBasicAuth(resMatch, "")
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(resMatch))))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
} else {
s1.Verified = false
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Column
}
func (s Scanner) Description() string {
return "Column is a service used for managing entity data. Column keys can be used to access and modify this data."
}
================================================
FILE: pkg/detectors/column/column_integration_test.go
================================================
//go:build detectors
// +build detectors
package column
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestColumn_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("COLUMN_ACTIVE")
inactiveSecret := testSecrets.MustGetField("COLUMN_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a column secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Column,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a column secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Column,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Column.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Column.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/column/column_test.go
================================================
package column
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Basic"
base_url: "https://api.example.com/v1/user"
column_key: "live_ID8Jxlu0QRsV7rKkWI9CUDpkrUv"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "live_ID8Jxlu0QRsV7rKkWI9CUDpkrUv"
)
func TestColumn_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/commercejs/commercejs.go
================================================
package commercejs
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"commercejs"}) + `\b([a-z0-9_]{48})\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{"commercejs"}
}
// FromData will find and optionally verify CommerceJS 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CommerceJS,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.chec.io/v1/categories", nil)
if err != nil {
continue
}
req.Header.Add("X-Authorization", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CommerceJS
}
func (s Scanner) Description() string {
return "CommerceJS is a headless commerce platform that provides APIs for building custom e-commerce experiences. CommerceJS API keys can be used to access and manage e-commerce functionalities."
}
================================================
FILE: pkg/detectors/commercejs/commercejs_integration_test.go
================================================
//go:build detectors
// +build detectors
package commercejs
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCommerceJS_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("COMMERCEJS")
inactiveSecret := testSecrets.MustGetField("COMMERCEJS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a commercejs secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CommerceJS,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a commercejs secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CommerceJS,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CommerceJS.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CommerceJS.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/commercejs/commercejs_test.go
================================================
package commercejs
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
in: "Header"
base_url: "https://api.example.com/v1/user"
commercejs_key: "g6cl4jt_2noyibalgbqid4h58jxivqdgyyxovepbvqmbl7wq"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "g6cl4jt_2noyibalgbqid4h58jxivqdgyyxovepbvqmbl7wq"
)
func TestCommerceJS_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/commodities/commodities.go
================================================
package commodities
import (
"context"
"io"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"commodities"}) + `\b([a-zA-Z0-9]{60})\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{"commodities"}
}
// FromData will find and optionally verify Commodities 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Commodities,
Raw: []byte(resMatch),
}
if verify {
client.Timeout = 5 * time.Second
req, err := http.NewRequestWithContext(ctx, "GET", "https://commodities-api.com/api/latest?access_key="+resMatch, nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
continue
}
bodyString := string(bodyBytes)
validResponse := strings.Contains(bodyString, `"success":true`)
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
if validResponse {
s1.Verified = true
} else {
s1.Verified = false
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Commodities
}
func (s Scanner) Description() string {
return "Commodities API keys can be used to access and modify commodity data."
}
================================================
FILE: pkg/detectors/commodities/commodities_integration_test.go
================================================
//go:build detectors
// +build detectors
package commodities
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCommodities_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("COMMODITIES")
inactiveSecret := testSecrets.MustGetField("COMMODITIES_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a commodities secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Commodities,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a commodities secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Commodities,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Commodities.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Commodities.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/commodities/commodities_test.go
================================================
package commodities
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
commodities_key: "5aJKBqkCyGCT9FIWNUTmowbzqgcm9DUCi60mHwgPQRBSt7dFahv9eY329Dn9"
base_url: "https://api.example.com/v1/user?access_key=$commodities_key"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "5aJKBqkCyGCT9FIWNUTmowbzqgcm9DUCi60mHwgPQRBSt7dFahv9eY329Dn9"
)
func TestCommodities_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/companyhub/companyhub.go
================================================
package companyhub
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"companyhub"}) + `\b([0-9a-zA-Z]{20})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"companyhub"}) + `\b([a-zA-Z0-9$%^=-]{4,32})\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{"companyhub"}
}
// FromData will find and optionally verify CompanyHub 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idmatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idmatch := range idmatches {
resIdMatch := strings.TrimSpace(idmatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CompanyHub,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.companyhub.com/v1/me", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("%s %s", resIdMatch, resMatch))
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CompanyHub
}
func (s Scanner) Description() string {
return "CompanyHub is a CRM tool used to manage customer relationships. CompanyHub keys can be used to access and manipulate CRM data."
}
================================================
FILE: pkg/detectors/companyhub/companyhub_integration_test.go
================================================
//go:build detectors
// +build detectors
package companyhub
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCompanyHub_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("COMPANYHUB_TOKEN")
inactiveSecret := testSecrets.MustGetField("COMPANYHUB_INACTIVE")
user := testSecrets.MustGetField("COMPANYHUB_USER")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a companyhub secret %s within companyhubuser %s", secret, user)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CompanyHub,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a companyhub secret %s within companyhubuser %s but not valid", inactiveSecret, user)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CompanyHub,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CompanyHub.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CompanyHub.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/companyhub/companyhub_test.go
================================================
package companyhub
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
in: "Header"
companyhub_key: "A5zrYt9xY4X1Q9mG6IX6"
companyhub_id: "xzAMMncOTR7^5d5NS6"
base_url: "https://api.example.com/v1/user"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secrets = []string{
"A5zrYt9xY4X1Q9mG6IX6xzAMMncOTR7^5d5NS6",
"A5zrYt9xY4X1Q9mG6IX6A5zrYt9xY4X1Q9mG6IX6",
}
)
func TestCompanyHub_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: secrets,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/confluent/confluent.go
================================================
package confluent
import (
"context"
b64 "encoding/base64"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"confluent"}) + `\b([a-zA-Z0-9]{16})\b`)
secretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"confluent"}) + `\b([a-zA-Z0-9\+\/]{64})\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{"confluent"}
}
// FromData will find and optionally verify Confluent 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
secretMatches := secretPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, secret := range secretMatches {
resSecret := strings.TrimSpace(secret[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Confluent,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resSecret),
}
if verify {
data := fmt.Sprintf("%s:%s", resMatch, resSecret)
sEnc := b64.StdEncoding.EncodeToString([]byte(data))
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.confluent.cloud/iam/v2/api-keys/"+resMatch, nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Confluent
}
func (s Scanner) Description() string {
return "Confluent provides a streaming platform based on Apache Kafka to help companies harness their data in real-time. Confluent API keys can be used to access and manage Kafka clusters."
}
================================================
FILE: pkg/detectors/confluent/confluent_integration_test.go
================================================
//go:build detectors
// +build detectors
package confluent
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestConfluent_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CONFLUENT_TOKEN")
key := testSecrets.MustGetField("CONFLUENT_KEY")
inactiveSecret := testSecrets.MustGetField("CONFLUENT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a confluent secret %s within confluent %s", secret, key)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Confluent,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a confluent secret %s within confluent %s but not valid", inactiveSecret, key)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Confluent,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Confluent.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Confluent.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/confluent/confluent_test.go
================================================
package confluent
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Basic"
in: "Header"
confluent_key: "CVAJHB4RAZboV3Od"
confluent_secret: "pIsdFuG0oJuyiir3GWqpC4pv7xpKFodCNh6PYN4XdE8EtyIwYtzEer0KtHQ8kofs"
base_url: "https://api.example.com/v1/user?api-key=$confluent_key"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "CVAJHB4RAZboV3OdpIsdFuG0oJuyiir3GWqpC4pv7xpKFodCNh6PYN4XdE8EtyIwYtzEer0KtHQ8kofs"
)
func TestConfluent_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/contentfulpersonalaccesstoken/contentfulpersonalaccesstoken.go
================================================
package contentfulpersonalaccesstoken
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
keyPat = regexp.MustCompile(`\b(CFPAT-[a-zA-Z0-9_\-]{43})\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{"CFPAT-"}
}
// FromData will find and optionally verify ContentfulDelivery 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)
keyMatches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range keyMatches {
keyRes := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ContentfulPersonalAccessToken,
Raw: []byte(keyRes),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.contentful.com/organizations", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", keyRes))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ContentfulPersonalAccessToken
}
func (s Scanner) Description() string {
return "Contentful is a content management system (CMS) that allows users to manage and deliver digital content. Contentful Personal Access Tokens can be used to access and modify this content."
}
================================================
FILE: pkg/detectors/contentfulpersonalaccesstoken/contentfulpersonalaccesstoken_test.go
================================================
package contentfulpersonalaccesstoken
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
contentful_access_token: "CFPAT-"
base_url: "https://api.example.com/v1/user"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
)
func TestContentfulPersonalAccessToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern - not found",
input: validPattern,
want: nil,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/contentfulpersonalaccesstoken/contentfulpersonalacesstoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package contentfulpersonalaccesstoken
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestContentfulPersonalAccessToken_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CONTENTFULPERSONALACCESSTOKEN")
inactiveSecret := testSecrets.MustGetField("CONTENTFULPERSONALACCESSTOKEN_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a contentful secret %s within contentful https://api.contentful.com/organizations", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ContentfulPersonalAccessToken,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a contentful secret %s within but unverified", inactiveSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ContentfulPersonalAccessToken,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("ContentfulPersonalAccessToken.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("ContentfulPersonalAccessToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/conversiontools/conversiontools.go
================================================
package conversiontools
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"conversiontools"}) + `\b(ey[a-zA-Z0-9_.]{157,165})\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{"conversiontools"}
}
// FromData will find and optionally verify ConversionTools 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ConversionTools,
Raw: []byte(resMatch),
}
if verify {
payload := strings.NewReader(`{ "type": "convert.website_to_jpg", "options": { "url": "http://google.com", "images": "yes" }}`)
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.conversiontools.io/v1/tasks", payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ConversionTools
}
func (s Scanner) Description() string {
return "ConversionTools is a service used for various data conversion tasks. The API keys can be used to access and perform these tasks."
}
================================================
FILE: pkg/detectors/conversiontools/conversiontools_integration_test.go
================================================
//go:build detectors
// +build detectors
package conversiontools
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestConversionTools_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CONVERSIONTOOLS")
inactiveSecret := testSecrets.MustGetField("CONVERSIONTOOLS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a conversiontools secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ConversionTools,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a conversiontools secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ConversionTools,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("ConversionTools.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("ConversionTools.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/conversiontools/conversiontools_test.go
================================================
package conversiontools
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
conversiontools_key: "ey5g5C73ichf2TWwQQfuPNG2SW1xdTmCHFgS6zsUjRz3kkiEofoa8X7SVGjwAMkhrv5KyOFqunP29gQpKq9A4sPF_Ps4B4IkTtgUG9cgP5A5ygAkuSR2rsOC.SIDSLIy4jZiL7L8ZHyAhyR8msV7JzxlI6YsNqmmj"
base_url: "https://api.example.com/v1/user"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "ey5g5C73ichf2TWwQQfuPNG2SW1xdTmCHFgS6zsUjRz3kkiEofoa8X7SVGjwAMkhrv5KyOFqunP29gQpKq9A4sPF_Ps4B4IkTtgUG9cgP5A5ygAkuSR2rsOC.SIDSLIy4jZiL7L8ZHyAhyR8msV7JzxlI6YsNqmmj"
)
func TestConversionTools_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/convertapi/convertapi.go
================================================
package convertapi
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"convertapi"}) + `\b(secret_[0-9a-zA-Z]{16})\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{"convertapi"}
}
// FromData will find and optionally verify ConvertApi 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ConvertApi,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://v2.convertapi.com/user?auth=%s", resMatch), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ConvertApi
}
func (s Scanner) Description() string {
return "ConvertAPI is a service that provides file conversion capabilities via API. ConvertAPI keys can be used to access and perform file conversions."
}
================================================
FILE: pkg/detectors/convertapi/convertapi_integration_test.go
================================================
//go:build detectors
// +build detectors
package convertapi
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestConvertApi_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CONVERTAPI")
inactiveSecret := testSecrets.MustGetField("CONVERTAPI_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a convertapi secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ConvertApi,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a convertapi secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ConvertApi,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("ConvertApi.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("ConvertApi.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/convertapi/convertapi_test.go
================================================
package convertapi
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
convertapi_key: "secret_H9ZGTfAERfN5W0AX"
base_url: "https://api.example.com/v1/user?auth=$convertapi_key"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "secret_H9ZGTfAERfN5W0AX"
)
func TestConvertAPI_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/convertkit/convertkit.go
================================================
package convertkit
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"convertkit"}) + `\b([a-z0-9A-Z_]{22})\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{"convertkit"}
}
// FromData will find and optionally verify Convertkit 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Convertkit,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.convertkit.com/v3/forms?api_key="+resMatch, nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Convertkit
}
func (s Scanner) Description() string {
return "Convertkit is an email marketing service provider. API keys can be used to access and manage email marketing campaigns and subscriber data."
}
================================================
FILE: pkg/detectors/convertkit/convertkit_integration_test.go
================================================
//go:build detectors
// +build detectors
package convertkit
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestConvertkit_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CONVERTKIT_TOKEN")
inactiveSecret := testSecrets.MustGetField("CONVERTKIT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a convertkit secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Convertkit,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a convertkit secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Convertkit,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Convertkit.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Convertkit.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/convertkit/convertkit_test.go
================================================
package convertkit
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
convertkit_key: "hfCnuVcYOgiRjlDEmAoRbN"
base_url: "https://api.example.com/v1/forms?api-key=$convertapi_key"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "hfCnuVcYOgiRjlDEmAoRbN"
)
func TestConvertKit_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/convier/convier.go
================================================
package convier
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"convier"}) + `\b([0-9]{2}\|[a-zA-Z0-9]{40})\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{"convier"}
}
// FromData will find and optionally verify Convier 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Convier,
Raw: []byte(resMatch),
}
if verify {
timeout := 10 * time.Second
client.Timeout = timeout
req, err := http.NewRequestWithContext(ctx, "POST", "https://convier.me/api/event", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
continue
}
bodyString := string(bodyBytes)
validResponse := strings.Contains(bodyString, `"error":false`)
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
if validResponse {
s1.Verified = true
} else {
s1.Verified = false
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Convier
}
func (s Scanner) Description() string {
return "Convier is a service for managing and verifying event data. Convier keys can be used to interact with the Convier API to manage event data."
}
================================================
FILE: pkg/detectors/convier/convier_integration_test.go
================================================
//go:build detectors
// +build detectors
package convier
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestConvier_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CONVIER")
inactiveSecret := testSecrets.MustGetField("CONVIER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a convier secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Convier,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a convier secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Convier,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Convier.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Convier.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/convier/convier_test.go
================================================
package convier
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
convier_key: "49|07KJBwfPzF2ESyNui5yBw9OVB6eWj0iXkssEKC7b"
base_url: "https://api.example.com/v1/user"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "49|07KJBwfPzF2ESyNui5yBw9OVB6eWj0iXkssEKC7b"
)
func TestConvier_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/copper/copper.go
================================================
package copper
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"copper"}) + `\b([a-z0-9]{32})\b`)
idPat = regexp.MustCompile(`\b([a-z0-9]{4,25}@[a-zA-Z0-9]{2,12}.[a-zA-Z0-9]{2,6})\b`)
)
type UserApiResponse struct {
Id int `json:"id"`
Email string `json:"email"`
}
// 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{"copper"}
}
// FromData will find and optionally verify Copper 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idmatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idmatch := range idmatches {
resIdMatch := strings.TrimSpace(idmatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Copper,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch),
}
if verify {
isVerified, verificationErr := verifyCopper(ctx, client, resIdMatch, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyCopper(ctx context.Context, client *http.Client, email, apiKey string) (bool, error) {
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
"https://api.copper.com/developer_api/v1/users/me",
http.NoBody,
)
if err != nil {
return false, err
}
req.Header.Add("X-PW-AccessToken", apiKey)
req.Header.Add("X-PW-Application", "developer_api")
req.Header.Add("X-PW-UserEmail", email)
req.Header.Add("Content-Type", "application/json")
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:
respBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
var respBody UserApiResponse
if err := json.Unmarshal(respBytes, &respBody); err != nil {
return false, err
}
// strict verification with email in credentials
if respBody.Email == email {
return true, nil
}
return false, fmt.Errorf("email mismatch in verification response")
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code :%d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Copper
}
func (s Scanner) Description() string {
return "Copper is a CRM platform that helps businesses manage their relationships with customers and leads. Copper API keys can be used to access and modify customer data and interactions."
}
================================================
FILE: pkg/detectors/copper/copper_integration_test.go
================================================
//go:build detectors
// +build detectors
package copper
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/detectorspb"
)
func TestCopper_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("COOPER_TOKEN")
inactiveSecret := testSecrets.MustGetField("COOPER_INACTIVE")
id := testSecrets.MustGetField("COOPER_ID")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a copper secret %s within copperid %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Copper,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a copper secret %s within copperid %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Copper,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Copper.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])
}
got[i].Raw = nil
got[i].RawV2 = nil
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "primarySecret")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Abstract.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/copper/copper_test.go
================================================
package copper
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
in: "Header"
copper_email: "s0ovh@P8I~p3"
copper_token: "noqs39jzqaegbam2k6mai9ov1uwsl21y"
base_url: "https://api.example.com/v1/user"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "noqs39jzqaegbam2k6mai9ov1uwsl21ys0ovh@P8I~p3"
)
func TestCopper_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/copy_metadata_test.go
================================================
package detectors
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
func TestCopyMetadata_ChunkDataFromOriginalData(t *testing.T) {
chunk := &sources.Chunk{
Data: []byte("decoded-data"),
OriginalData: []byte("original-source-data"),
SourceName: "test-source",
}
result := Result{
DetectorType: 1,
Raw: []byte("secret"),
}
rwm := CopyMetadata(chunk, result)
assert.Equal(t, "original-source-data", string(rwm.ChunkData))
}
func TestCopyMetadata_ChunkDataFallsBackToData(t *testing.T) {
chunk := &sources.Chunk{
Data: []byte("only-data"),
SourceName: "test-source",
}
result := Result{
DetectorType: 1,
Raw: []byte("secret"),
}
rwm := CopyMetadata(chunk, result)
assert.Equal(t, "only-data", string(rwm.ChunkData))
}
================================================
FILE: pkg/detectors/couchbase/couchbase.go
================================================
package couchbase
import (
"context"
"fmt"
"time"
"unicode"
regexp "github.com/wasilibs/go-re2"
"github.com/couchbase/gocb/v2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
connectionStringPat = regexp.MustCompile(`\b(cb\.[a-z0-9]+\.cloud\.couchbase\.com)\b`)
usernamePat = common.UsernameRegexCheck(`?()/\+=\s\n`)
passwordPat = common.PasswordRegexCheck(`^<>;.*&|£\n\s`)
)
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Couchbase
}
func (s Scanner) Description() string {
return "Couchbase is a distributed NoSQL cloud database. Couchbase credentials can be used to access and modify data within the Couchbase database."
}
// 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{"couchbase://", "couchbases://"}
}
// FromData will find and optionally verify Couchbase 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)
var uniqueConnStrings, uniqueUsernames, uniquePasswords = make(map[string]struct{}), make(map[string]struct{}), make(map[string]struct{})
for _, match := range connectionStringPat.FindAllStringSubmatch(dataStr, -1) {
uniqueConnStrings["couchbases://"+match[1]] = struct{}{}
}
for _, match := range usernamePat.Matches(data) {
uniqueUsernames[match] = struct{}{}
}
for _, match := range passwordPat.Matches(data) {
uniquePasswords[match] = struct{}{}
}
for connString := range uniqueConnStrings {
for username := range uniqueUsernames {
for password := range uniquePasswords {
if !isValidCouchbasePassword(password) {
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Couchbase,
Raw: fmt.Appendf([]byte(""), "%s:%s@%s", username, password, connString),
}
if verify {
isVerified, verificationErr := verifyCouchBase(username, password, connString)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
s1.SetPrimarySecretValue(connString)
}
results = append(results, s1)
}
}
}
return results, nil
}
func verifyCouchBase(username, password, connString string) (bool, error) {
options := gocb.ClusterOptions{
Authenticator: gocb.PasswordAuthenticator{
Username: username,
Password: password,
},
}
// Sets a pre-configured profile called "wan-development" to help avoid latency issues
// when accessing Capella from a different Wide Area Network
// or Availability Zone (e.g. your laptop).
if err := options.ApplyProfile(gocb.ClusterConfigProfileWanDevelopment); err != nil {
return false, err
}
// Initialize the Connection
cluster, err := gocb.Connect(connString, options)
if err != nil {
return false, err
}
// We'll ping the KV nodes in our cluster.
pings, err := cluster.Ping(&gocb.PingOptions{
Timeout: time.Second * 5,
})
if err != nil {
return false, err
}
for _, ping := range pings.Services {
for _, pingEndpoint := range ping {
if pingEndpoint.State == gocb.PingStateOk {
return true, nil
}
}
}
return false, nil
}
func isValidCouchbasePassword(password string) bool {
var hasLower, hasUpper, hasNumber, hasSpecialChar bool
for _, r := range password {
switch {
case unicode.IsLower(r):
hasLower = true
case unicode.IsUpper(r):
hasUpper = true
case unicode.IsNumber(r):
hasNumber = true
case unicode.IsPunct(r), unicode.IsSymbol(r):
hasSpecialChar = true
}
}
return hasLower && hasUpper && hasNumber && hasSpecialChar
}
================================================
FILE: pkg/detectors/couchbase/couchbase_integration_test.go
================================================
//go:build detectors
// +build detectors
package couchbase
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCouchbase_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)
}
endpoint := testSecrets.MustGetField("COUCHBASE_ENDPOINT")
username := testSecrets.MustGetField("COUCHBASE_USERNAME")
password := testSecrets.MustGetField("COUCHBASE_PASSWORD")
inactiveSecret := testSecrets.MustGetField("COUCHBASE_INACTIVE_PASSWORD")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("db uri: %s \n username = %s \n password = %s", endpoint, username, password)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Couchbase,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("db uri: %s \n username = %s \n password = %s", endpoint, username, inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Couchbase,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Couchbase.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])
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "primarySecret")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Couchbase.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/couchbase/couchbase_test.go
================================================
package couchbase
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
func TestCouchBase_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Password"
in: "Configuration"
couchbase_domain: "couchbases://cb.testing.cloud.couchbase.com"
couchbase_username: "usrpS@d>p"
couchbase_password: "passwordU+2028 rf\@V[4,L/?2}"
base_url: "https://$couchbase_domain/v1/user"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`,
want: []string{
"usrpS@d>p:passwordU+2028@couchbases://cb.testing.cloud.couchbase.com",
"$DB_USERNAME:passwordU+2028@couchbases://cb.testing.cloud.couchbase.com",
},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/countrylayer/countrylayer.go
================================================
package countrylayer
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"countrylayer"}) + `\b([a-z0-9]{32})\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{"countrylayer"}
}
// FromData will find and optionally verify CountryLayer 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CountryLayer,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.countrylayer.com/v2/all?access_key=%s", resMatch), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CountryLayer
}
func (s Scanner) Description() string {
return "CountryLayer is a service that provides information about countries. CountryLayer API keys can be used to access this information."
}
================================================
FILE: pkg/detectors/countrylayer/countrylayer_integration_test.go
================================================
//go:build detectors
// +build detectors
package countrylayer
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCountryLayer_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("COUNTRYLAYER")
inactiveSecret := testSecrets.MustGetField("COUNTRYLAYER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a countrylayer secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CountryLayer,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a countrylayer secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CountryLayer,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CountryLayer.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CountryLayer.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/countrylayer/countrylayer_test.go
================================================
package countrylayer
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
countrylayer_key: "031eiaqplnq39py5ppsctxo6n2xj5t10"
base_url: "https://api.example.com/v1/user?access_key=$countrylayer_key"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "031eiaqplnq39py5ppsctxo6n2xj5t10"
)
func TestCountryLayer_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/courier/courier.go
================================================
package courier
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"courier"}) + `\b(pk\_[a-zA-Z0-9]{1,}\_[a-zA-Z0-9]{28})\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{"courier"}
}
// FromData will find and optionally verify Courier 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Courier,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.courier.com/preferences", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Courier
}
func (s Scanner) Description() string {
return "Courier is a notification service that allows developers to send notifications through multiple channels. Courier API keys can be used to manage and send notifications."
}
================================================
FILE: pkg/detectors/courier/courier_integration_test.go
================================================
//go:build detectors
// +build detectors
package courier
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCourier_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("COURIER")
inactiveSecret := testSecrets.MustGetField("COURIER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a courier secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Courier,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a courier secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Courier,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Courier.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Courier.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/courier/courier_test.go
================================================
package courier
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
courier_key: "pk_iHWk6NqTne0QthfSVF7uixZpa3OTYpA8hC6bIIavhluXfxz37FB_rHPqJNWh06HpNIOuokET4dfFzXS1"
base_url: "https://api.example.com/v1/user"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "pk_iHWk6NqTne0QthfSVF7uixZpa3OTYpA8hC6bIIavhluXfxz37FB_rHPqJNWh06HpNIOuokET4dfFzXS1"
)
func TestCourier_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/coveralls/coveralls.go
================================================
package coveralls
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"coveralls"}) + `\b([a-zA-Z0-9-]{37})\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{"coveralls"}
}
// FromData will find and optionally verify Coveralls 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Coveralls,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://coveralls.io/api/repos/github/secretscanner02/scanner", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("token %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Coveralls
}
func (s Scanner) Description() string {
return "Coveralls is a web service to help you track your code coverage over time, and ensure that all your new code is fully covered. Coveralls tokens can be used to access and modify coverage data."
}
================================================
FILE: pkg/detectors/coveralls/coveralls_integration_test.go
================================================
//go:build detectors
// +build detectors
package coveralls
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCoveralls_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("COVERALLS_TOKEN")
inactiveSecret := testSecrets.MustGetField("COVERALLS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a coveralls secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Coveralls,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a coveralls secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Coveralls,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Coveralls.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Coveralls.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/coveralls/coveralls_test.go
================================================
package coveralls
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
in: "Header"
coveralls_token: "tPfhjkzKJyWtUdxDLYMjNDEfP7Yn9WvWb2-K3"
base_url: "https://api.example.com/v1/user"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "tPfhjkzKJyWtUdxDLYMjNDEfP7Yn9WvWb2-K3"
)
func TestCoveralls_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/craftmypdf/craftmypdf.go
================================================
package craftmypdf
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"craftmypdf"}) + `\b([0-9a-zA-Z]{35})\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{"craftmypdf"}
}
// FromData will find and optionally verify CraftMyPDF 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CraftMyPDF,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.craftmypdf.com/v1/get-account-info", nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-API-KEY", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CraftMyPDF
}
func (s Scanner) Description() string {
return "CraftMyPDF is a service for generating PDFs from templates and data. CraftMyPDF API keys can be used to access and manage PDF generation tasks."
}
================================================
FILE: pkg/detectors/craftmypdf/craftmypdf_integration_test.go
================================================
//go:build detectors
// +build detectors
package craftmypdf
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCraftMyPDF_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CRAFTMYPDF")
inactiveSecret := testSecrets.MustGetField("CRAFTMYPDF_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a craftmypdf secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CraftMyPDF,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a craftmypdf secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CraftMyPDF,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CraftMyPDF.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CraftMyPDF.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/craftmypdf/craftmypdf_test.go
================================================
package craftmypdf
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
craftmypdf_key: "GuTSS3XQdT6fx00mxudKq7oj2CsieZCGmEc"
base_url: "https://api.example.com/v1/user"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "GuTSS3XQdT6fx00mxudKq7oj2CsieZCGmEc"
)
func TestCraftMyPDF_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/crowdin/crowdin.go
================================================
package crowdin
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"crowdin"}) + `\b([0-9A-Za-z]{80})\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{"crowdin"}
}
// FromData will find and optionally verify Crowdin 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Crowdin,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.crowdin.com/api/v2/storages", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Crowdin
}
func (s Scanner) Description() string {
return "Crowdin is a cloud-based localization management platform. Crowdin API keys can be used to access and manage localization projects and resources."
}
================================================
FILE: pkg/detectors/crowdin/crowdin_integration_test.go
================================================
//go:build detectors
// +build detectors
package crowdin
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCrowdin_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CROWDIN")
inactiveSecret := testSecrets.MustGetField("CROWDIN_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a crowdin secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Crowdin,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a crowdin secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Crowdin,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Crowdin.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
for i := range got {
if len(got[i].Raw) == 0 {
t.Fatal("no raw secret present")
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Crowdin.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/crowdin/crowdin_test.go
================================================
package crowdin
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
crowdin_token: "BiIRgdPvboWwqlhQtlnCsM041zVYCJ5yMfgltWesDiu9bv1nuRtCEPewsDL3vgRFcp2qLemaPMa8L9g7"
base_url: "https://api.example.com/v1/user"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "BiIRgdPvboWwqlhQtlnCsM041zVYCJ5yMfgltWesDiu9bv1nuRtCEPewsDL3vgRFcp2qLemaPMa8L9g7"
)
func TestCrowDin_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/cryptocompare/cryptocompare.go
================================================
package cryptocompare
import (
"context"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cryptocompare"}) + `\b([a-z-0-9]{64})\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{"cryptocompare"}
}
// FromData will find and optionally verify CryptoCompare 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CryptoCompare,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://min-api.cryptocompare.com/data/blockchain/list?api_key="+resMatch, nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
continue
}
bodyString := string(bodyBytes)
errCode := strings.Contains(bodyString, `"Response":"Success"`)
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
if errCode {
s1.Verified = true
} else {
s1.Verified = false
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CryptoCompare
}
func (s Scanner) Description() string {
return "CryptoCompare is a cryptocurrency market data provider. CryptoCompare API keys can be used to access and retrieve market data."
}
================================================
FILE: pkg/detectors/cryptocompare/cryptocompare_integration_test.go
================================================
//go:build detectors
// +build detectors
package cryptocompare
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCryptoCompare_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CRYPTOCOMPARE")
inactiveSecret := testSecrets.MustGetField("CRYPTOCOMPARE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cryptocompare secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CryptoCompare,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a cryptocompare secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CryptoCompare,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CryptoCompare.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CryptoCompare.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/cryptocompare/cryptocompare_test.go
================================================
package cryptocompare
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
cryptocompare_key: "lx8zzovs5h15zl15mj224zks2v25re59965gz0l1z4jsc0bng33a75m5pf52-bvd"
base_url: "https://api.example.com/v1/user?api_key=$cryptocompare_key"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "lx8zzovs5h15zl15mj224zks2v25re59965gz0l1z4jsc0bng33a75m5pf52-bvd"
)
func TestCryptoCompare_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/currencycloud/currencycloud.go
================================================
package currencycloud
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"currencycloud"}) + `\b([0-9a-z]{64})\b`)
emailPat = regexp.MustCompile(common.EmailPattern)
)
// 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{"currencycloud"}
}
// FromData will find and optionally verify Currencycloud 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
uniqueEmailMatches := make(map[string]struct{})
for _, match := range emailPat.FindAllStringSubmatch(dataStr, -1) {
uniqueEmailMatches[strings.TrimSpace(match[1])] = struct{}{}
}
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for emailmatch := range uniqueEmailMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CurrencyCloud,
Raw: []byte(resMatch),
}
environments := []string{"devapi", "api"}
if verify {
for _, env := range environments {
// Get authentication token
payload := strings.NewReader(`{"login_id":"` + emailmatch + `","api_key":"` + resMatch + `"`)
req, err := http.NewRequestWithContext(ctx, "POST", "https://"+env+".currencycloud.com/v2/authenticate/api", payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
continue
}
body := string(bodyBytes)
if strings.Contains(body, "auth_token") {
s1.Verified = true
s1.ExtraData = map[string]string{"environment": fmt.Sprintf("https://%s.currencycloud.com", env)}
break
}
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CurrencyCloud
}
func (s Scanner) Description() string {
return "Currencycloud provides a global payments platform that allows businesses to make payments and manage currency risk. Currencycloud API keys can be used to access and manage these financial services."
}
================================================
FILE: pkg/detectors/currencycloud/currencycloud_integration_test.go
================================================
//go:build detectors
// +build detectors
package currencycloud
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCurrencycloud_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CURRENCYCLOUD")
email := testSecrets.MustGetField("SCANNERS_EMAIL")
inactiveSecret := testSecrets.MustGetField("CURRENCYCLOUD_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a currencycloud secret %s within %s", secret, email)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CurrencyCloud,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a currencycloud secret %s within %s but not valid", inactiveSecret, email)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CurrencyCloud,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Currencycloud.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])
}
got[i].Raw = nil
got[i].ExtraData = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Currencycloud.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/currencycloud/currencycloud_test.go
================================================
package currencycloud
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 = "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b / testuser1005@example.com"
invalidPattern = "abcD123efg456HIJklmn789OPQ_rstUVWxYZ-012/testing@go"
)
func TestCurrencyCloud_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: fmt.Sprintf("currencycloud: %s", validPattern),
want: []string{"1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b"},
},
{
name: "valid pattern - key out of prefix range",
input: fmt.Sprintf("currencycloud keyword is not close to the real key and id = %s", validPattern),
want: nil,
},
{
name: "invalid pattern",
input: fmt.Sprintf("currencycloud: %s", invalidPattern),
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 && test.want != nil {
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) {
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)
}
})
}
}
================================================
FILE: pkg/detectors/currencyfreaks/currencyfreaks.go
================================================
package currencyfreaks
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"currencyfreaks"}) + `\b([0-9a-z]{32})\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{"currencyfreaks"}
}
// FromData will find and optionally verify Currencyfreaks 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Currencyfreaks,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.currencyfreaks.com/latest?apikey="+resMatch+"&format=xml", nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Currencyfreaks
}
func (s Scanner) Description() string {
return "Currencyfreaks provides exchange rates and currency conversion API services. The API keys can be used to access and retrieve exchange rate data."
}
================================================
FILE: pkg/detectors/currencyfreaks/currencyfreaks_integration_test.go
================================================
//go:build detectors
// +build detectors
package currencyfreaks
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCurrencyfreaks_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CURRENCYFREAKS")
inactiveSecret := testSecrets.MustGetField("CURRENCYFREAKS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a currencyfreaks secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Currencyfreaks,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a currencyfreaks secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Currencyfreaks,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Currencyfreaks.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Currencyfreaks.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/currencyfreaks/currencyfreaks_test.go
================================================
package currencyfreaks
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
currencyfreaks_key: "6zlrpo4u8z4s72b2nqr54m9ehmqvwe8p"
base_url: "https://api.example.com/v1/user?apiKey=$currencyfreaks_key"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "6zlrpo4u8z4s72b2nqr54m9ehmqvwe8p"
)
func TestCurrencyFreaks_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/currencylayer/currencylayer.go
================================================
package currencylayer
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"currencylayer"}) + `\b([a-z0-9]{32})\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{"currencylayer"}
}
// FromData will find and optionally verify Currencylayer 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Currencylayer,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.currencylayer.com/live?access_key=%s", resMatch), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
bodyBytes, err2 := io.ReadAll(res.Body)
if err2 == nil {
bodyString := string(bodyBytes)
validResponse := strings.Contains(bodyString, `"success": true`) || strings.Contains(bodyString, `"info":"Access Restricted - Your current Subscription Plan does not support HTTPS Encryption."`)
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
if validResponse {
s1.Verified = true
} else {
s1.Verified = false
}
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Currencylayer
}
func (s Scanner) Description() string {
return "An API for converting and exchanging currencies. API keys can read currency data."
}
================================================
FILE: pkg/detectors/currencylayer/currencylayer_integration_test.go
================================================
//go:build detectors
// +build detectors
package currencylayer
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCurrencylayer_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CURRENCYLAYER")
inactiveSecret := testSecrets.MustGetField("CURRENCYLAYER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a currencylayer secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Currencylayer,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a currencylayer secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Currencylayer,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Currencylayer.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
for i := range got {
if len(got[i].Raw) == 0 {
t.Fatal("no raw secret present")
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Currencylayer.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/currencylayer/currencylayer_test.go
================================================
package currencylayer
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
currencylayer_key: "sxthwp257vpusfe4gr4d4awc794lkxvh"
base_url: "https://api.example.com/v1/user?access_key=$currencylayer_key"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "sxthwp257vpusfe4gr4d4awc794lkxvh"
)
func TestCurrencyLayer_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/currencyscoop/currencyscoop.go
================================================
package currencyscoop
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"currencyscoop"}) + `\b([a-z0-9]{32})\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{"currencyscoop"}
}
// FromData will find and optionally verify Currencyscoop 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CurrencyScoop,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.currencyscoop.com/v1/latest?api_key=%s", resMatch), nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CurrencyScoop
}
func (s Scanner) Description() string {
return "CurrencyScoop is a currency data service providing real-time and historical exchange rates. CurrencyScoop API keys can be used to access currency data."
}
================================================
FILE: pkg/detectors/currencyscoop/currencyscoop_integration_test.go
================================================
//go:build detectors
// +build detectors
package currencyscoop
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCurrencyscoop_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CURRENCYSCOOP")
inactiveSecret := testSecrets.MustGetField("CURRENCYSCOOP_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a currencyscoop secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CurrencyScoop,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a currencyscoop secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CurrencyScoop,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Currencyscoop.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Currencyscoop.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/currencyscoop/currencyscoop_test.go
================================================
package currencyscoop
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
currencyscoop_key: "70x6tezndca5dqlm5tnn7s03bm6c27jt"
base_url: "https://api.example.com/v1/user?api_key=$currencylayer_key"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "70x6tezndca5dqlm5tnn7s03bm6c27jt"
)
func TestCurrencyScoop_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/currentsapi/currentsapi.go
================================================
package currentsapi
import (
"context"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"currentsapi"}) + `([a-zA-Z0-9_-]{48})`)
)
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CurrentsAPI
}
func (s Scanner) Description() string {
return "CurrentsAPI provides access to the latest news and trends. CurrentsAPI keys can be used to authenticate requests and retrieve news data."
}
// 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{"currentsapi"}
}
// FromData will find and optionally verify CurrentsAPI 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)
var uniqueTokens = make(map[string]struct{})
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueTokens[match[1]] = struct{}{}
}
for token := range uniqueTokens {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CurrentsAPI,
Raw: []byte(token),
}
if verify {
isVerified, verificationErr := verifyCurrentsAPI(ctx, client, token)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, token)
}
results = append(results, s1)
}
return results, nil
}
func verifyCurrentsAPI(ctx context.Context, client *http.Client, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.currentsapi.services/v1/latest-news", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Authorization", token)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/currentsapi/currentsapi_integration_test.go
================================================
//go:build detectors
// +build detectors
package currentsapi
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCurrentsAPI_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CURRENTSAPI")
inactiveSecret := testSecrets.MustGetField("CURRENTSAPI_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a currentsapi secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CurrentsAPI,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a currentsapi secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CurrentsAPI,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CurrentsAPI.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CurrentsAPI.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/currentsapi/currentsapi_test.go
================================================
package currentsapi
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
func TestCurrentsAPI_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
in: "Header"
currentsapi_key: "P1ctBOMKKnSnc43K6z5E1IiPp0Q46BTrf62UHJBTcC2qkCGE"
base_url: "https://api.example.com/v1/user"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`,
want: []string{"P1ctBOMKKnSnc43K6z5E1IiPp0Q46BTrf62UHJBTcC2qkCGE"},
},
{
name: "valid pattern",
input: `
GLOBAL{currentsapi}{AQAAABAAA -WE1-BwePKJJwiRN0lZ_qBe4WpZpgeAeYy281o5nImlhqaxG}configuration for production2023-05-18T14:32:10Zjenkins-admin
`,
want: []string{"-WE1-BwePKJJwiRN0lZ_qBe4WpZpgeAeYy281o5nImlhqaxG"},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/customerguru/customerguru.go
================================================
package customerguru
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"guru"}) + `\b([a-z0-9A-Z]{30})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"guru"}) + `\b([a-z0-9A-Z]{50})\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{"customerguru"}
}
// FromData will find and optionally verify CustomerGuru 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idmatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idmatch := range idmatches {
resIdMatch := strings.TrimSpace(idmatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CustomerGuru,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://customer.guru/export/customers?api_secret="+resIdMatch+"&api_token="+resMatch, nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CustomerGuru
}
func (s Scanner) Description() string {
return "CustomerGuru is a feedback platform used to collect and analyze customer feedback. API keys and secrets can be used to access and manage this feedback data."
}
================================================
FILE: pkg/detectors/customerguru/customerguru_integration_test.go
================================================
//go:build detectors
// +build detectors
package customerguru
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCustomerGuru_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CUSTOMERGURU_TOKEN")
inactiveSecret := testSecrets.MustGetField("CUSTOMERGURU_INACTIVE")
key := testSecrets.MustGetField("CUSTOMERGURU_KEY")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a customerguru secret %s within customergurukey %s", secret, key)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CustomerGuru,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a customerguru secret %s within customergurukey %s but not valid", inactiveSecret, key)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CustomerGuru,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CustomerGuru.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CustomerGuru.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/customerguru/customerguru_test.go
================================================
package customerguru
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
guru_key: "WWj2zAK0tMkVJqc28Itfu6THQycyfT"
guru_id: "Ic53IHpPK71wIacbCgEkIlFbw0VIMcsz6ir2i2DJ0XDRdirf2K"
base_url: "https://api.customerguru.com/v1/user?api_secret=$guru_id&api_token=guru_key"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "WWj2zAK0tMkVJqc28Itfu6THQycyfTIc53IHpPK71wIacbCgEkIlFbw0VIMcsz6ir2i2DJ0XDRdirf2K"
)
func TestCustomerGuru_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/customerio/customerio.go
================================================
package customerio
import (
"context"
b64 "encoding/base64"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"customer"}) + `\b([a-z0-9A-Z]{20})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"customer"}) + `\b([a-z0-9A-Z]{20})\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{"customerio"}
}
// FromData will find and optionally verify CustomerIO 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idmatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idmatch := range idmatches {
resIdMatch := strings.TrimSpace(idmatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_CustomerIO,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch),
}
if verify {
payload := strings.NewReader("name=purchase&data%5Bprice%5D=23.45&data%5Bproduct%5D=socks")
data := fmt.Sprintf("%s:%s", resIdMatch, resMatch)
sEnc := b64.StdEncoding.EncodeToString([]byte(data))
req, err := http.NewRequestWithContext(ctx, "POST", "https://track.customer.io/api/v1/customers/5/events", payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_CustomerIO
}
func (s Scanner) Description() string {
return "CustomerIO is a platform for sending automated emails, push notifications, and SMS messages. CustomerIO API keys can be used to interact with the CustomerIO service to manage customer data and trigger events."
}
================================================
FILE: pkg/detectors/customerio/customerio_integration_test.go
================================================
//go:build detectors
// +build detectors
package customerio
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestCustomerIO_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("CUSTOMERIO_TOKEN")
inactiveSecret := testSecrets.MustGetField("CUSTOMERIO_INACTIVE")
id := testSecrets.MustGetField("CUSTOMERIO_ID")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a customerio secret %s within customerid %s ", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CustomerIO,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a customerio secret %s within customerid %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_CustomerIO,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("CustomerIO.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("CustomerIO.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/customerio/customerio_test.go
================================================
package customerio
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Basic"
in: "Header"
customerio_key: "bXQLU0kcl0A7kxCErc3L"
customerio_id: "tM2JFc8pmKHUmkdwhmgG"
base_url: "https://api.example.com/v1/user?access_key=$currencylayer_key"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secrets = []string{
"bXQLU0kcl0A7kxCErc3LbXQLU0kcl0A7kxCErc3L",
"bXQLU0kcl0A7kxCErc3LtM2JFc8pmKHUmkdwhmgG",
"tM2JFc8pmKHUmkdwhmgGbXQLU0kcl0A7kxCErc3L",
"tM2JFc8pmKHUmkdwhmgGtM2JFc8pmKHUmkdwhmgG",
}
)
func TestCustomerio_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: secrets,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/d7network/d7network.go
================================================
package d7network
import (
"context"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"d7network"}) + `\b([a-zA-Z0-9\W\S]{23}\=)`)
)
// 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{"d7network"}
}
// FromData will find and optionally verify D7Network 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_D7Network,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://rest-api.d7networks.com/secure/balance", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", "Basic "+resMatch)
res, err := detectors.DetectorHttpClientWithNoLocalAddresses.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_D7Network
}
func (s Scanner) Description() string {
return "D7Network provides messaging services through their API. The credentials can be used to send SMS and other types of messages via their platform."
}
================================================
FILE: pkg/detectors/d7network/d7network_integration_test.go
================================================
//go:build detectors
// +build detectors
package d7network
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestD7Network_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("D7NETWORK_TOKEN")
inactiveSecret := testSecrets.MustGetField("D7NETWORK_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a d7network secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_D7Network,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a d7network secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_D7Network,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("D7Network.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("D7Network.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/d7network/d7network_test.go
================================================
package d7network
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Basic"
in: "Header"
d7network_secret: "u@D7GXt)t>8d(LtH^(lvZ 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)
}
})
}
}
================================================
FILE: pkg/detectors/dailyco/dailyco.go
================================================
package dailyco
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"daily"}) + `\b([0-9a-f]{64})\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{"daily"}
}
// FromData will find and optionally verify DailyCO 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_DailyCO,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.daily.co/v1/rooms", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_DailyCO
}
func (s Scanner) Description() string {
return "DailyCO is a video calling service that provides APIs to create and manage video calls. The API keys can be used to access and control these video call services."
}
================================================
FILE: pkg/detectors/dailyco/dailyco_integration_test.go
================================================
//go:build detectors
// +build detectors
package dailyco
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDailyCO_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DAILYCO")
inactiveSecret := testSecrets.MustGetField("DAILYCO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dailyco secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DailyCO,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dailyco secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DailyCO,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("DailyCO.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("DailyCO.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/dailyco/dailyco_test.go
================================================
package dailyco
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
dailyco_secret: "40842f16899170ffaf4e8ea99c68e748fac5e9ee5d675dd06fbe0c300a8f291a"
base_url: "https://api.example.com/v1/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "40842f16899170ffaf4e8ea99c68e748fac5e9ee5d675dd06fbe0c300a8f291a"
)
func TestDailyCo_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/dandelion/dandelion.go
================================================
package dandelion
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"dandelion"}) + `\b([a-z0-9]{32})\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{"dandelion"}
}
// FromData will find and optionally verify Dandelion 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Dandelion,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.dandelion.eu/datatxt/li/v1/?text=Smart&token=%s", resMatch), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Dandelion
}
func (s Scanner) Description() string {
return "Dandelion is a text analysis service. Dandelion tokens can be used to access and analyze text data using their APIs."
}
================================================
FILE: pkg/detectors/dandelion/dandelion_integration_test.go
================================================
//go:build detectors
// +build detectors
package dandelion
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDandelion_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DANDELION")
inactiveSecret := testSecrets.MustGetField("DANDELION_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dandelion secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dandelion,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dandelion secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dandelion,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Dandelion.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Dandelion.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/dandelion/dandelion_test.go
================================================
package dandelion
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
dandelion_secret: "xccl325526f9cp6qzh89qkgoklje5ds9"
base_url: "https://api.example.com/v1/example?token=$dandelion_secret"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "xccl325526f9cp6qzh89qkgoklje5ds9"
)
func TestDandelion_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/dareboost/dareboost.go
================================================
package dareboost
import (
"context"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"dareboost"}) + `\b([0-9a-zA-Z]{60})\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{"dareboost"}
}
// FromData will find and optionally verify Dareboost 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Dareboost,
Raw: []byte(resMatch),
}
if verify {
payload := strings.NewReader(`{ "token": "` + resMatch + `", "location": "Paris"}`)
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.dareboost.com/0.8/config", payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
continue
}
bodyString := string(bodyBytes)
validResponse := strings.Contains(bodyString, `"status":200`)
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
if validResponse {
s1.Verified = true
} else {
s1.Verified = false
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Dareboost
}
func (s Scanner) Description() string {
return "Dareboost is a website performance monitoring tool. Dareboost API keys can be used to access and modify performance monitoring configurations."
}
================================================
FILE: pkg/detectors/dareboost/dareboost_integration_test.go
================================================
//go:build detectors
// +build detectors
package dareboost
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDareboost_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DAREBOOST")
inactiveSecret := testSecrets.MustGetField("DAREBOOST_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dareboost secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dareboost,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dareboost secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dareboost,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Dareboost.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Dareboost.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/dareboost/dareboost_test.go
================================================
package dareboost
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
in: "Body"
dareboost_secret: "fS6aBVkb0qpOje4VED8OhKqGGNdNVUuDhdBi9fTvxwIRMNK2uyd68WlPa1X5"
body: {"payload":$dareboost_secret}
base_url: "https://api.example.com/v1/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "fS6aBVkb0qpOje4VED8OhKqGGNdNVUuDhdBi9fTvxwIRMNK2uyd68WlPa1X5"
)
func TestDareBoost_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/databox/databox.go
================================================
package databox
import (
"context"
b64 "encoding/base64"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"databox"}) + common.BuildRegex(common.RegexPattern, "", 21))
)
// 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{"databox"}
}
// FromData will find and optionally verify Databox 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Databox,
Raw: []byte(resMatch),
}
if verify {
data := fmt.Sprintf("%s:", resMatch)
sEnc := b64.StdEncoding.EncodeToString([]byte(data))
payload := strings.NewReader(`{
"data":[
{
"$sales": 420,
"$visitors": 123000
}
]
}`)
req, err := http.NewRequestWithContext(ctx, "POST", "https://push.databox.com", payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/vnd.databox.v2+json")
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Databox
}
func (s Scanner) Description() string {
return "Databox is a business analytics platform that pulls all your data into one place, so you can track performance and discover insights in real-time. Databox API keys can be used to access and modify data within your Databox account."
}
================================================
FILE: pkg/detectors/databox/databox_integration_test.go
================================================
//go:build detectors
// +build detectors
package databox
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDatabox_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DATABOX")
inactiveSecret := testSecrets.MustGetField("DATABOX_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a databox secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Databox,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a databox secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Databox,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Databox.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Databox.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/databox/databox_test.go
================================================
package databox
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Basic"
in: "Header"
databox_secret: "arjrvgzxx20sivy4rigjs"
base_url: "https://api.example.com/v1/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "arjrvgzxx20sivy4rigjs"
)
func TestDataBox_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/databrickstoken/databrickstoken.go
================================================
package databrickstoken
import (
"context"
"fmt"
"io"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
domain = regexp.MustCompile(`\b([a-z0-9-]+(?:\.[a-z0-9-]+)*\.(cloud\.databricks\.com|gcp\.databricks\.com|azuredatabricks\.net))\b`)
keyPat = regexp.MustCompile(`\b(dapi[0-9a-f]{32}(-\d)?)\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{"databricks", "dapi"}
}
// FromData will find and optionally verify Databrickstoken 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)
var uniqueDomains, uniqueTokens = make(map[string]struct{}), make(map[string]struct{})
for _, match := range domain.FindAllStringSubmatch(dataStr, -1) {
uniqueDomains[strings.TrimSpace(match[1])] = struct{}{}
}
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueTokens[match[1]] = struct{}{}
}
for token := range uniqueTokens {
for domain := range uniqueDomains {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_DatabricksToken,
Raw: []byte(token),
RawV2: []byte(token + domain),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyDatabricksToken(client, domain, token)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
if s1.Verified {
s1.AnalysisInfo = map[string]string{
"token": token,
"domain": domain,
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_DatabricksToken
}
func (s Scanner) Description() string {
return "Databricks is a cloud data platform. Databricks tokens can be used to authenticate and interact with Databricks services and APIs."
}
func verifyDatabricksToken(client *http.Client, domain, token string) (bool, error) {
req, err := http.NewRequest(http.MethodGet, "https://"+domain+"/api/2.0/preview/scim/v2/Me", nil)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/databrickstoken/databrickstoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package databrickstoken
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDatabricksToken_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("DATABRICKSTOKEN")
inactiveSecret := testSecrets.MustGetField("DATABRICKSTOKEN_INACTIVE")
domain := testSecrets.MustGetField("DATABRICKSTOKEN_DOMAIN")
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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a databrickstoken secret %s within %s", secret, domain)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DatabricksToken,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a databrickstoken secret %s within %s but not valid", inactiveSecret, domain)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DatabricksToken,
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 databrickstoken secret %s within %s", secret, domain)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DatabricksToken,
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 databrickstoken secret %s within %s", secret, domain)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DatabricksToken,
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("Databrickstoken.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "AnalysisInfo")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("DatabricksToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/databrickstoken/databrickstoken_test.go
================================================
package databrickstoken
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
secret: "dapib8a799e452bf722cb28874cee50a7abf"
domain: "nonprod-test.cloud.databricks.com"
base_url: "https://$domain/v1/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "dapib8a799e452bf722cb28874cee50a7abfnonprod-test.cloud.databricks.com"
)
func TestDataBrickStoken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/datadogapikey/datadogapikey.go
================================================
package datadogapikey
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
regexp "github.com/wasilibs/go-re2"
)
type Scanner struct {
client *http.Client
detectors.EndpointSetter
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.EndpointCustomizer = (*Scanner)(nil)
var _ detectors.CloudProvider = (*Scanner)(nil)
func (Scanner) CloudEndpoint() string { return "https://api.datadoghq.com" }
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
apiKeyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"datadog", "dd"}) + `\b([a-zA-Z0-9-]{32})\b`)
datadogURLPat = regexp.MustCompile(`\b(api(?:\.[a-z0-9-]+)?\.(?:datadoghq|ddog-gov)\.(com|eu))\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{"datadog", "ddog-gov"}
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return client
}
// FromData will find and optionally verify DatadogToken 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)
apiMatches := apiKeyPat.FindAllStringSubmatch(dataStr, -1)
var uniqueFoundUrls = make(map[string]struct{})
for _, matches := range datadogURLPat.FindAllStringSubmatch(dataStr, -1) {
uniqueFoundUrls[matches[1]] = struct{}{}
}
endpoints := make([]string, 0, len(uniqueFoundUrls))
for endpoint := range uniqueFoundUrls {
endpoints = append(endpoints, "https://"+endpoint)
}
for _, apiMatch := range apiMatches {
resApiMatch := strings.TrimSpace(apiMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_DatadogApikey,
Raw: []byte(resApiMatch),
}
if verify {
for _, baseURL := range s.Endpoints(endpoints...) {
client := s.getClient()
isVerified, verificationErr := verifyMatch(ctx, client, resApiMatch, baseURL)
if isVerified {
s1.Verified = isVerified
s1.AnalysisInfo = map[string]string{"api_key": resApiMatch, "endpoint": baseURL}
// break the loop once we've successfully validated the token against a baseURL
break
}
s1.SetVerificationError(verificationErr, resApiMatch)
}
}
results = append(results, s1)
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, apiKey, baseUrl string) (bool, error) {
// Reference: https://docs.datadoghq.com/api/latest/authentication/
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseUrl+"/api/v1/validate", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("DD-API-KEY", apiKey)
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:
return false, nil
case http.StatusTooManyRequests:
return false, fmt.Errorf("too many requests")
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_DatadogApikey
}
func (s Scanner) Description() string {
return "Datadog is a monitoring and security platform for cloud applications. Datadog API and Application keys can be used to access and manage data and configurations within Datadog."
}
================================================
FILE: pkg/detectors/datadogapikey/datadogapikey_integration_test.go
================================================
//go:build detectors
// +build detectors
package datadogapikey
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDataDogApiKey_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)
}
apiKey := testSecrets.MustGetField("DATADOGTOKEN_TOKEN")
invalidApiKey := "FKNwdbyfYTmGUm5DK3yHEuK-BBQf0fVG"
datdogEndpoint := "https://api.us5.datadoghq.com"
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a datadogtoken secret within datadog %s and endpoint is %v", apiKey, datdogEndpoint)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DatadogApikey,
Verified: true,
AnalysisInfo: map[string]string{
"api_key": apiKey,
"endpoint": datdogEndpoint,
},
Raw: []byte(apiKey),
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a datadogtoken secret within datadog %s and endpoint is %v", invalidApiKey, datdogEndpoint)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DatadogApikey,
Verified: false,
Raw: []byte(invalidApiKey),
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
// use default cloud endpoint
s.UseCloudEndpoint(true)
s.SetCloudEndpoint(s.CloudEndpoint())
s.UseFoundEndpoints(true)
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("DatadogToken.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("DatadogToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/datadogapikey/datadogapikey_test.go
================================================
package datadogapikey
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDataDogApiKey_Pattern_WithValidAPIKey(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
input := `
dd_api_secret: "FKNwdbyfYTmGUm5DK3yHEuK-BBQf0fVG"
dd_app: "iHxNanzZ8vjrmbjXK7NJLrwpGw2czdSh90PKH6VL"
base_url1: "https://api.us5.datadoghq.com"
base_url2: "https://api.app.ddog-gov.com"
`
apiKey := "FKNwdbyfYTmGUm5DK3yHEuK-BBQf0fVG"
wantedResult := []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DatadogApikey,
Raw: []byte(apiKey),
},
}
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(input))
if len(matchedDetectors) == 0 {
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), input)
return
}
results, err := d.FromData(context.Background(), false, []byte(input))
if err != nil {
t.Errorf("error = %v", err)
return
}
if diff := cmp.Diff(wantedResult, results, cmpopts.IgnoreFields(detectors.Result{}, "verificationError", "primarySecret")); diff != "" {
t.Errorf("%s diff: (-want +got)\n%s", "TestDataDogApiKey_Pattern_WithValidAPIKeyOnly", diff)
}
}
func TestDataDogApiKey_NoSecrets(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
input := `
base_url1: "https://api.us5.datadoghq.com"
base_url2: "https://api.app.ddog-gov.com"
`
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(input))
if len(matchedDetectors) == 0 {
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), input)
return
}
results, err := d.FromData(context.Background(), false, []byte(input))
if err != nil {
t.Errorf("error = %v", err)
return
}
if len(results) != 0 {
t.Errorf("expected 0 results, received %d", len(results))
}
}
func TestDataDogApiKey_InvalidSecrets(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
input := `
dd_api_secret: "@FKNwdbyfYTmGUm5DK3yHEuK"
dd_app: "iHxNanzZ8vjrmbjXK7NJLrwpGw2czdSh90PKH6VL"
base_url1: "https://api.us5.datadoghq.com"
base_url2: "https://api.app.ddog-gov.com"
`
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(input))
if len(matchedDetectors) == 0 {
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), input)
return
}
results, err := d.FromData(context.Background(), false, []byte(input))
if err != nil {
t.Errorf("error = %v", err)
return
}
if len(results) != 0 {
t.Errorf("expected 0 results, received %d", len(results))
}
}
================================================
FILE: pkg/detectors/datadogtoken/datadogtoken.go
================================================
package datadogtoken
import (
"context"
"encoding/json"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.EndpointSetter
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.EndpointCustomizer = (*Scanner)(nil)
var _ detectors.CloudProvider = (*Scanner)(nil)
func (Scanner) CloudEndpoint() string { return "https://api.datadoghq.com" }
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
appPat = regexp.MustCompile(detectors.PrefixRegex([]string{"datadog", "dd"}) + `\b([a-zA-Z-0-9]{40})\b`)
apiPat = regexp.MustCompile(detectors.PrefixRegex([]string{"datadog", "dd"}) + `\b([a-zA-Z-0-9]{32})\b`)
datadogURLPat = regexp.MustCompile(`\b(api(?:\.[a-z0-9-]+)?\.(?:datadoghq|ddog-gov)\.(com|eu))\b`)
)
type userServiceResponse struct {
Data []*user `json:"data"`
Included []*options `json:"included"`
}
type user struct {
Attributes userAttributes `json:"attributes"`
}
type userAttributes struct {
Email string `json:"email"`
IsServiceAccount bool `json:"service_account"`
Verified bool `json:"verified"`
Disabled bool `json:"disabled"`
}
type options struct {
Type string `json:"type"`
Attributes optionAttribute `json:"attributes"`
}
type optionAttribute struct {
Url string `json:"url"`
Name string `json:"name"`
Disabled bool `json:"disabled"`
}
func setUserEmails(data []*user, s1 *detectors.Result) {
var emails []string
for _, user := range data {
// filter out non verified emails, disabled emails, service accounts
if user.Attributes.Verified && !user.Attributes.Disabled && !user.Attributes.IsServiceAccount {
emails = append(emails, user.Attributes.Email)
}
}
if len(emails) == 0 && len(data) > 0 {
emails = append(emails, data[0].Attributes.Email)
}
s1.ExtraData["user_emails"] = strings.Join(emails, ", ")
}
func setOrganizationInfo(opt []*options, s1 *detectors.Result) {
var orgs *options
for _, option := range opt {
if option.Type == "orgs" && !option.Attributes.Disabled {
orgs = option
break
}
}
if orgs != nil {
s1.ExtraData["org_name"] = orgs.Attributes.Name
s1.ExtraData["org_url"] = orgs.Attributes.Url
}
}
// 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{"datadog", "ddog-gov"}
}
// FromData will find and optionally verify DatadogToken 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)
appMatches := appPat.FindAllStringSubmatch(dataStr, -1)
apiMatches := apiPat.FindAllStringSubmatch(dataStr, -1)
var uniqueFoundUrls = make(map[string]struct{})
for _, matches := range datadogURLPat.FindAllStringSubmatch(dataStr, -1) {
uniqueFoundUrls["https://"+matches[1]] = struct{}{}
}
endpoints := make([]string, 0, len(uniqueFoundUrls))
for endpoint := range uniqueFoundUrls {
endpoints = append(endpoints, endpoint)
}
for _, apiMatch := range apiMatches {
resApiMatch := strings.TrimSpace(apiMatch[1])
for _, appMatch := range appMatches {
resAppMatch := strings.TrimSpace(appMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_DatadogToken,
Raw: []byte(resAppMatch),
RawV2: []byte(resAppMatch + resApiMatch),
ExtraData: map[string]string{
"Type": "Application+APIKey",
},
}
if verify {
for _, baseURL := range s.Endpoints(endpoints...) {
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/api/v2/users", nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("DD-API-KEY", resApiMatch)
req.Header.Add("DD-APPLICATION-KEY", resAppMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
s1.AnalysisInfo = map[string]string{"api_key": resApiMatch, "app_key": resAppMatch, "endpoint": baseURL}
var serviceResponse userServiceResponse
if err := json.NewDecoder(res.Body).Decode(&serviceResponse); err == nil {
// setup emails
if len(serviceResponse.Data) > 0 {
setUserEmails(serviceResponse.Data, &s1)
}
// setup organizations
if len(serviceResponse.Included) > 0 {
setOrganizationInfo(serviceResponse.Included, &s1)
}
}
// break the loop once we've successfully validated the token against a baseURL
break
}
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_DatadogToken
}
func (s Scanner) Description() string {
return "Datadog is a monitoring and security platform for cloud applications. Datadog API and Application keys can be used to access and manage data and configurations within Datadog."
}
================================================
FILE: pkg/detectors/datadogtoken/datadogtoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package datadogtoken
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDatadogToken_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)
}
apiKey := testSecrets.MustGetField("DATADOGTOKEN_TOKEN")
appKey := testSecrets.MustGetField("DATADOGTOKEN_APPKEY")
inactiveAppKey := testSecrets.MustGetField("DATADOGTOKEN_INACTIVE")
endpoint := "https://api.us5.datadoghq.com"
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a datadogtoken secret %s within datadog %s and endpoint %s", appKey, apiKey, endpoint)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DatadogToken,
Verified: true,
ExtraData: map[string]string{
"Type": "Application+APIKey",
},
AnalysisInfo: map[string]string{
"api_key": apiKey,
"app_key": appKey,
"endpoint": endpoint,
},
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a datadogtoken secret %s within but datadog %s not valid", inactiveAppKey, apiKey)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DatadogToken,
Verified: false,
ExtraData: map[string]string{
"Type": "Application+APIKey",
},
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
// use default cloud endpoint
s.UseCloudEndpoint(true)
s.SetCloudEndpoint(s.CloudEndpoint())
s.UseFoundEndpoints(true)
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("DatadogToken.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])
}
got[i].Raw = nil
got[i].RawV2 = nil
delete(got[i].ExtraData, "user_emails")
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("DatadogToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/datadogtoken/datadogtoken_test.go
================================================
package datadogtoken
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
func TestDataDogToken_Pattern_WithValidAPIandAppKey(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
input := `
dd_api_secret: "FKNwdbyfYTmGUm5DK3yHEuK-BBQf0fVG"
dd_app: "iHxNanzZ8vjrmbjXK7NJLrwpGw2czdSh90PKH6VL"
base_url1: "https://api.us5.datadoghq.com"
base_url2: "https://api.app.ddog-gov.com"
`
want := []string{"iHxNanzZ8vjrmbjXK7NJLrwpGw2czdSh90PKH6VLFKNwdbyfYTmGUm5DK3yHEuK-BBQf0fVG"}
wantedResultType := "Application+APIKey"
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(input))
if len(matchedDetectors) == 0 {
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), input)
return
}
results, err := d.FromData(context.Background(), false, []byte(input))
if err != nil {
t.Errorf("error = %v", err)
return
}
if len(results) != len(want) {
if len(results) == 0 {
t.Errorf("did not receive result")
} else {
t.Errorf("expected %d results, only received %d", len(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{}{}
}
if r.ExtraData["Type"] != wantedResultType {
t.Errorf("expected result type %s, got %s", wantedResultType, r.ExtraData["Type"])
}
}
expected := make(map[string]struct{}, len(want))
for _, v := range want {
expected[v] = struct{}{}
}
if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf("%s diff: (-want +got)\n%s", "TestDataDogToken_Pattern_WithValidAPIandAppKey", diff)
}
}
func TestDataDogToken_Pattern_WithAPIKeyOnly(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
input := `
dd_api_secret: "FKNwdbyfYTmGUm5DK3yHEuK-BBQf0fVG"
base_url: "https://api.us5.datadoghq.com"
response_code: 200
`
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(input))
if len(matchedDetectors) == 0 {
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), input)
return
}
results, err := d.FromData(context.Background(), false, []byte(input))
if err != nil {
t.Errorf("error = %v", err)
return
}
if len(results) != 0 {
t.Errorf("expected 0 results, received %d", len(results))
}
}
func TestDataDogToken_NoSecrets(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
input := `
base_url1: "https://api.us5.datadoghq.com"
base_url2: "https://api.app.ddog-gov.com"
`
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(input))
if len(matchedDetectors) == 0 {
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), input)
return
}
results, err := d.FromData(context.Background(), false, []byte(input))
if err != nil {
t.Errorf("error = %v", err)
return
}
if len(results) != 0 {
t.Errorf("expected 0 results, received %d", len(results))
}
}
func TestDataDogToken_InvalidSecrets(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
input := `
dd_api_secret: "@FKNwdbyfYTmGUm5DK3yHEuK"
dd_app: "iHxNanzZ8vjrmbjXK7NJLrwpGw2czdSh90PKH6VL"
base_url1: "https://api.us5.datadoghq.com"
base_url2: "https://api.app.ddog-gov.com"
`
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(input))
if len(matchedDetectors) == 0 {
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), input)
return
}
results, err := d.FromData(context.Background(), false, []byte(input))
if err != nil {
t.Errorf("error = %v", err)
return
}
if len(results) != 0 {
t.Errorf("expected 0 results, received %d", len(results))
}
}
================================================
FILE: pkg/detectors/datagov/datagov.go
================================================
package datagov
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"data.gov"}) + `\b([a-zA-Z0-9]{40})\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{"data.gov"}
}
// FromData will find and optionally verify DataGov 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_DataGov,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.ers.usda.gov/data/arms/state?api_key=%s", resMatch), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_DataGov
}
func (s Scanner) Description() string {
return "Data.gov provides access to datasets generated by the U.S. government. The API key can be used to access and retrieve data from these datasets."
}
================================================
FILE: pkg/detectors/datagov/datagov_integration_test.go
================================================
//go:build detectors
// +build detectors
package datagov
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDataGov_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DATAGOV")
inactiveSecret := testSecrets.MustGetField("DATAGOV_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a data.gov secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DataGov,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a data.gov secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DataGov,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("DataGov.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("DataGov.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/datagov/datagov_test.go
================================================
package datagov
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
data.gov_secret: "Ge4R2TmPk1R6NPsXYu0ceRnmawtYfnVeiZ4zztB8"
base_url: "https://api.example.com/v1/example?api_key=$data.gov_secret"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "Ge4R2TmPk1R6NPsXYu0ceRnmawtYfnVeiZ4zztB8"
)
func TestDataGov_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/debounce/debounce.go
================================================
package debounce
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"debounce"}) + `\b([a-zA-Z0-9]{13})\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{"debounce"}
}
// FromData will find and optionally verify Debounce 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Debounce,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.debounce.io/v1/?api="+resMatch+"&email=some@gmail.com", nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Debounce
}
func (s Scanner) Description() string {
return "Debounce is an email validation service that helps in reducing bounce rates by verifying email addresses. Debounce API keys can be used to access and validate email addresses."
}
================================================
FILE: pkg/detectors/debounce/debounce_integration_test.go
================================================
//go:build detectors
// +build detectors
package debounce
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDebounce_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DEBOUNCE_TOKEN")
inactiveSecret := testSecrets.MustGetField("DEBOUNCE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a debounce secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Debounce,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a debounce secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Debounce,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Debounce.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Debounce.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/debounce/debounce_test.go
================================================
package debounce
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
debounce_secret: "OTM0Bp42sFTRB"
base_url: "https://api.example.com/v1/example?api=$debounce_secret"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "OTM0Bp42sFTRB"
)
func TestDebounce_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/deepai/deepai.go
================================================
package deepai
import (
"bytes"
"context"
"io"
"mime/multipart"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"deepai"}) + `\b([a-z0-9-]{36})\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{"deepai"}
}
// FromData will find and optionally verify DeepAI 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_DeepAI,
Raw: []byte(resMatch),
}
if verify {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
fw, err := writer.CreateFormField("text")
if err != nil {
continue
}
_, err = io.Copy(fw, strings.NewReader("test"))
if err != nil {
continue
}
writer.Close()
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.deepai.org/api/text-tagging", bytes.NewReader(body.Bytes()))
if err != nil {
continue
}
req.Header.Add("Content-Type", writer.FormDataContentType())
req.Header.Add("api-key", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_DeepAI
}
func (s Scanner) Description() string {
return "DeepAI is an AI service provider offering various machine learning APIs. DeepAI API keys can be used to access and utilize these services."
}
================================================
FILE: pkg/detectors/deepai/deepai_integration_test.go
================================================
//go:build detectors
// +build detectors
package deepai
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDeepAI_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DEEPAI")
inactiveSecret := testSecrets.MustGetField("DEEPAI_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a deepai secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DeepAI,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a deepai secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DeepAI,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("DeepAI.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("DeepAI.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/deepai/deepai_test.go
================================================
package deepai
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
deepai_secret: "ulrouaemk45y6pr8clttmjw8sucqq3skl7g9"
base_url: "https://api.example.com/v1/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "ulrouaemk45y6pr8clttmjw8sucqq3skl7g9"
)
func TestDeepAI_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/deepgram/deepgram.go
================================================
package deepgram
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"deepgram"}) + `\b([0-9a-z]{40})\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{"deepgram"}
}
// FromData will find and optionally verify Deepgram 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Deepgram,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.deepgram.com/v1/projects", nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Token %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Deepgram
}
func (s Scanner) Description() string {
return "Deepgram is an automatic speech recognition (ASR) service. Deepgram API keys can be used to access and utilize Deepgram's ASR capabilities."
}
================================================
FILE: pkg/detectors/deepgram/deepgram_integration_test.go
================================================
//go:build detectors
// +build detectors
package deepgram
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDeepgram_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DEEPGRAM")
inactiveSecret := testSecrets.MustGetField("DEEPGRAM_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a deepgram secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Deepgram,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a deepgram secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Deepgram,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Deepgram.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Deepgram.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/deepgram/deepgram_test.go
================================================
package deepgram
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Token"
in: "Header"
deepgram_secret: "4y7fjndvwi8bydxfwe0zppeef9n6j44kpizq3zr4"
base_url: "https://api.example.com/v1/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "4y7fjndvwi8bydxfwe0zppeef9n6j44kpizq3zr4"
)
func TestDeepGram_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/deepseek/deepseek.go
================================================
package deepseek
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"deepseek"}) + `\b(sk-[a-z0-9]{32})\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{"deepseek"}
}
// FromData will find and optionally verify DeepSeek 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: detectorspb.DetectorType_DeepSeek,
Raw: []byte(token),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
verified, extraData, verificationErr := verifyToken(ctx, client, token)
s1.Verified = verified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return
}
func verifyToken(ctx context.Context, client *http.Client, token string) (bool, map[string]string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.deepseek.com/user/balance", nil)
if err != nil {
return false, nil, err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
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()
}()
switch res.StatusCode {
case http.StatusOK:
var resData response
if err = json.NewDecoder(res.Body).Decode(&resData); err != nil {
return false, nil, err
}
extraData := map[string]string{
"is_available": fmt.Sprintf("%t", resData.IsAvailable),
}
return true, extraData, nil
case http.StatusUnauthorized:
// Invalid
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_DeepSeek
}
func (s Scanner) Description() string {
return "DeepSeek is an artificial intelligence company that develops large language models (LLMs)"
}
type response struct {
IsAvailable bool `json:"is_available"`
}
================================================
FILE: pkg/detectors/deepseek/deepseek_integration_test.go
================================================
//go:build detectors
// +build detectors
package deepseek
import (
"context"
"fmt"
"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/detectorspb"
"testing"
"time"
)
func TestDeepseek_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)
}
apiKey := testSecrets.MustGetField("DEEPSEEK")
inactiveSecret := testSecrets.MustGetField("DEEPSEEK_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a deepseek secret %s within", apiKey)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DeepSeek,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a deepseek secret %s within but not valid", inactiveSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DeepSeek,
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 deepseek secret %s within", apiKey)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DeepSeek,
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("Deepseek.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
// Ignore Extra Data for comparison
if tt.want[i].Verified == true {
if got[i].ExtraData != nil {
got[i].ExtraData = nil
} else {
t.Fatalf("no extra data")
}
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "AnalysisInfo")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Deepseek.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/deepseek/deepseek_test.go
================================================
package deepseek
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
func TestDeepseek_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
other.code()
deepseek.Apikey = sk-abc123def456ghi789jkl012mno345pq
`,
want: []string{
"sk-abc123def456ghi789jkl012mno345pq",
},
},
{
name: "invalid pattern",
input: "deepseek.key = sk-abc123invalid",
want: nil,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/delighted/delighted.go
================================================
package delighted
import (
"context"
b64 "encoding/base64"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"delighted"}) + `\b([a-z0-9A-Z]{32})\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{"delighted"}
}
// FromData will find and optionally verify Delighted 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Delighted,
Raw: []byte(resMatch),
}
if verify {
data := fmt.Sprintf("%s:", resMatch)
sEnc := b64.StdEncoding.EncodeToString([]byte(data))
payload := strings.NewReader(`{
"email": "jony@appleseed.com",
"properties": { "Purchase Experience": "Mobile App", "State": "CA" }
}`)
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.delighted.com/v1/people.json", payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Delighted
}
func (s Scanner) Description() string {
return "Delighted is a customer feedback platform. Delighted API keys can be used to access and manage customer feedback data."
}
================================================
FILE: pkg/detectors/delighted/delighted_integration_test.go
================================================
//go:build detectors
// +build detectors
package delighted
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDelighted_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DELIGHTED_TOKEN")
inactiveSecret := testSecrets.MustGetField("DELIGHTED_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a delighted secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Delighted,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a delighted secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Delighted,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Delighted.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Delighted.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/delighted/delighted_test.go
================================================
package delighted
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Basic"
in: "Header"
delighted_secret: "Vm62eJY7FFguRjYjqIdiLXUEOoRgvQ6W"
base_url: "https://api.example.com/v1/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "Vm62eJY7FFguRjYjqIdiLXUEOoRgvQ6W"
)
func TestDelighted_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/demio/demio.go
================================================
package demio
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"demio"}) + `\b([a-z0-9A-Z]{32})\b`)
secretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"demio"}) + `\b([a-z0-9A-Z]{10,20})\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{"demio"}
}
// FromData will find and optionally verify Demio 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := secretPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idmatch := range idMatches {
resIdMatch := strings.TrimSpace(idmatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Demio,
Raw: []byte(resMatch),
}
if verify {
url := fmt.Sprintf("https://my.demio.com/api/v1/ping/query?api_key=%s&api_secret=%s", resMatch, resIdMatch)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Demio
}
func (s Scanner) Description() string {
return "Demio is a webinar platform that allows users to host, promote, and analyze webinars. Demio API keys can be used to access and manage webinar data."
}
================================================
FILE: pkg/detectors/demio/demio_integration_test.go
================================================
//go:build detectors
// +build detectors
package demio
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDemio_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DEMIO")
inactiveSecret := testSecrets.MustGetField("DEMIO_INACTIVE")
keySecret := testSecrets.MustGetField("DEMIO_SECRET")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a demio secret %s within demio %s", secret, keySecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Demio,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a demio secret %s within but not valid demio %s", inactiveSecret, keySecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Demio,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Demio.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Demio.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/demio/demio_test.go
================================================
package demio
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
demio_key: "KL0F0y61VeIixRmn2A4Sha3h0xiLMX7J"
demio_secret: "PWkiVWEw7s7JjtzR"
base_url: "https://api.example.com/v1/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "KL0F0y61VeIixRmn2A4Sha3h0xiLMX7J"
)
func TestDemio_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/deno/denodeploy.go
================================================
package denodeploy
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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
tokenPat = regexp.MustCompile(`\b(dd[pw]_[a-zA-Z0-9]{36})\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{"ddp_", "ddw_"}
}
type userResponse struct {
Login string `json:"login"`
}
// FromData will find and optionally verify DenoDeploy 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)
tokenMatches := tokenPat.FindAllStringSubmatch(dataStr, -1)
for _, tokenMatch := range tokenMatches {
token := tokenMatch[1]
s1 := detectors.Result{
DetectorType: s.Type(),
Raw: []byte(token),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.deno.com/user", nil)
req.Header.Set("Authorization", "Bearer "+token)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode == 200 {
s1.Verified = true
body, err := io.ReadAll(res.Body)
if err != nil {
s1.SetVerificationError(err, token)
} else {
var user userResponse
if err := json.Unmarshal(body, &user); err != nil {
s1.SetVerificationError(err, token)
} else {
s1.ExtraData = map[string]string{
"login": user.Login,
}
}
}
} else if res.StatusCode == 401 {
// The secret is determinately not verified (nothing to do)
} else {
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
s1.SetVerificationError(err, token)
}
} else {
s1.SetVerificationError(err, token)
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_DenoDeploy
}
func (s Scanner) Description() string {
return "DenoDeploy is a cloud service for deploying JavaScript and TypeScript applications. DenoDeploy tokens can be used to access and manage these deployments."
}
================================================
FILE: pkg/detectors/deno/denodeploy_integration_test.go
================================================
//go:build detectors
// +build detectors
package denodeploy
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDenoDeploy_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("DENODEPLOY")
inactiveSecret := testSecrets.MustGetField("DENODEPLOY_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a denodeploy secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DenoDeploy,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a denodeploy secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DenoDeploy,
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 denodeploy secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DenoDeploy,
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 denodeploy secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DenoDeploy,
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("Denodeploy.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "ExtraData")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Denodeploy.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/deno/denodeploy_test.go
================================================
package denodeploy
import (
"context"
"testing"
)
func TestDenoDeploy_Pattern(t *testing.T) {
tests := []struct {
name string
data string
shouldMatch bool
match string
}{
// True positives
{
name: `valid_deployctl`,
data: ` "tasks": {
"d": "deployctl deploy --prod --import-map=import_map.json --project=o88 main.ts --token ddp_eg5DjUmbR5lHZ3LiN9MajMk2tA1GxL2NRdvc",
"start": "deno run -A --unstable --watch=static/,routes/ dev.ts"
},`,
shouldMatch: true,
match: `ddp_eg5DjUmbR5lHZ3LiN9MajMk2tA1GxL2NRdvc`,
},
{
name: `valid_dotenv`,
data: `DENO_KV_ACCESS_TOKEN=ddp_hn029Cl2dIN4Jb0BF0L1V9opokoPVC30ddGk`,
shouldMatch: true,
match: `ddp_hn029Cl2dIN4Jb0BF0L1V9opokoPVC30ddGk`,
},
{
name: `valid_dotfile`,
data: `# deno
export DENO_INSTALL="/home/khushal/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"
export DENO_DEPLOY_TOKEN="ddp_QLbDfRlMKpXSf3oCz20Hp8wVVxThDwlwhFbV""`,
shouldMatch: true,
match: `ddp_QLbDfRlMKpXSf3oCz20Hp8wVVxThDwlwhFbV`,
},
{
name: `valid_webtoken`,
data: ` // headers: { Authorization: 'Bearer ddw_ebahKKeZqiZVXOad7KJRHskLeP79Lf0OJXlj' }`,
shouldMatch: true,
match: `ddw_ebahKKeZqiZVXOad7KJRHskLeP79Lf0OJXlj`,
},
// False positives
{
name: `invalid_token1`,
data: ` "summoner2Id": 4,
"summonerId": "oljqJ1Ddp_LJm5s6ONPAJXIl97Bi6pcKMywYLG496a58rA",
"summonerLevel": 146,`,
shouldMatch: false,
},
{
name: `invalid_token2`,
data: ` "image_thumbnail_url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQFq6zzTXpXtRDdP_JbNkS58loAyCvhhZ1WWONaUkJoWbHsgwIJBw",`,
shouldMatch: false,
},
{
name: `invalid_token3`,
data: `matplotlib/backends/_macosx.cpython-37m-darwin.so,sha256=DDw_KRE5yTUEY5iDBwBW7KvDcTkDmrIu0N18i8I3FvA,90140`,
shouldMatch: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := Scanner{}
results, err := s.FromData(context.Background(), false, []byte(test.data))
if err != nil {
t.Errorf("DenoDeploy.FromData() error = %v", err)
return
}
if test.shouldMatch {
if len(results) == 0 {
t.Errorf("%s: did not receive a match for '%v' when one was expected", test.name, test.data)
return
}
expected := test.data
if test.match != "" {
expected = test.match
}
result := results[0]
resultData := string(result.Raw)
if resultData != expected {
t.Errorf("%s: did not receive expected match.\n\texpected: '%s'\n\t actual: '%s'", test.name, expected, resultData)
return
}
} else {
if len(results) > 0 {
t.Errorf("%s: received a match for '%v' when one wasn't wanted", test.name, test.data)
return
}
}
})
}
}
================================================
FILE: pkg/detectors/deputy/deputy.go
================================================
package deputy
import (
"context"
"fmt"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = detectors.DetectorHttpClientWithNoLocalAddresses
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"deputy"}) + `\b([0-9a-z]{32})\b`)
urlPat = regexp.MustCompile(`\b([0-9a-z]{1,}\.as\.deputy\.com)\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{"deputy"}
}
// FromData will find and optionally verify Deputy 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
urlMatches := urlPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, urlMatch := range urlMatches {
resURL := strings.TrimSpace(urlMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Deputy,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://%s/api/v1/me", resURL), nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("OAuth %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Deputy
}
func (s Scanner) Description() string {
return "Deputy is a workforce management software that provides various tools for scheduling, time tracking, and communication. Deputy API keys can be used to access and modify data within the Deputy platform."
}
================================================
FILE: pkg/detectors/deputy/deputy_integration_test.go
================================================
//go:build detectors
// +build detectors
package deputy
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDeputy_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DEPUTY")
url := testSecrets.MustGetField("DEPUTY_URL")
inactiveSecret := testSecrets.MustGetField("DEPUTY_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a deputy secret %s within %s", secret, url)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Deputy,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a deputy secret %s within %s but not valid", inactiveSecret, url)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Deputy,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Deputy.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Deputy.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/deputy/deputy_test.go
================================================
package deputy
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
deputy_secret: "puf5nguo090lkrkqxfeqj5ymm0nb26pt"
base_url: "https://api.nonprodtest.as.deputy.com.com/v1/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "puf5nguo090lkrkqxfeqj5ymm0nb26pt"
)
func TestDeputy_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/detectify/detectify.go
================================================
package detectify
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"detectify"}) + `\b([0-9a-z]{32})\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{"detectify"}
}
// FromData will find and optionally verify Detectify 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Detectify,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.detectify.com/rest/v2/assets/", nil)
if err != nil {
continue
}
req.Header.Add("X-Detectify-Key", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Detectify
}
func (s Scanner) Description() string {
return "Detectify is a web application security scanner that helps identify vulnerabilities in web applications. Detectify API keys can be used to access and manage security scans and findings."
}
================================================
FILE: pkg/detectors/detectify/detectify_integration_test.go
================================================
//go:build detectors
// +build detectors
package detectify
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDetectify_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DETECTIFY")
inactiveSecret := testSecrets.MustGetField("DETECTIFY_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a detectify secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Detectify,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a detectify secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Detectify,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Detectify.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Detectify.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/detectify/detectify_test.go
================================================
package detectify
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
detectify_secret: "eg90srff9v6cxk794kr2k56l5q5s9wx2"
base_url: "https://api.example.com/v1/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "eg90srff9v6cxk794kr2k56l5q5s9wx2"
)
func TestDetectify_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/detectlanguage/detectlanguage.go
================================================
package detectlanguage
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"detectlanguage"}) + `\b([a-z0-9]{32})\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{"detectlanguage"}
}
// FromData will find and optionally verify DetectLanguage 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_DetectLanguage,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://ws.detectlanguage.com/0.2/user/status", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_DetectLanguage
}
func (s Scanner) Description() string {
return "DetectLanguage is a language detection API service. The API key can be used to access the language detection functionalities provided by DetectLanguage."
}
================================================
FILE: pkg/detectors/detectlanguage/detectlanguage_integration_test.go
================================================
//go:build detectors
// +build detectors
package detectlanguage
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDetectLanguage_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DETECTLANGUAGE")
inactiveSecret := testSecrets.MustGetField("DETECTLANGUAGE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a detectlanguage secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DetectLanguage,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a detectlanguage secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DetectLanguage,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("DetectLanguage.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("DetectLanguage.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/detectlanguage/detectlanguage_test.go
================================================
package detectlanguage
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
detectlanguage_secret: "6esicmhsdpu8blum1wzr8a6bae9s507u"
base_url: "https://api.example.com/v1/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "6esicmhsdpu8blum1wzr8a6bae9s507u"
)
func TestDetectLanguage_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/detectors.go
================================================
package detectors
import (
"context"
"crypto/rand"
"errors"
"math/big"
"net/url"
"strings"
"unicode"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
// Detector defines an interface for scanning for and verifying secrets.
type Detector interface {
// FromData will scan bytes for results and optionally verify them.
//
// FromData can be called concurrently from multiple goroutines.
// Any modification to the receiver or to global variables will need to use some kind of synchronization.
FromData(ctx context.Context, verify bool, data []byte) ([]Result, error)
// Keywords are used for efficiently pre-filtering chunks using substring operations.
// Use unique identifiers that are part of the secret if you can, or the provider name.
//
// When multiple keywords are provided, they are is treated as a *union* of filtering terms.
// That is, if any of the keywords are found in a chunk, the chunk will be run through the detector.
Keywords() []string
// Type returns the DetectorType number from detectors.proto for the given detector.
Type() detectorspb.DetectorType
// Description returns a description for the result being detected
Description() string
}
// CustomResultsCleaner is an optional interface that a detector can implement to customize how its generated results
// are "cleaned," which is defined as removing superfluous results from those found in a given chunk. The default
// implementation of this logic removes all unverified results if there are any verified results, and all unverified
// results except for one otherwise, but this interface allows a detector to specify different logic. (This logic must
// be implemented outside results generation because there are circumstances under which the engine should not execute
// it.)
type CustomResultsCleaner interface {
// CleanResults removes "superfluous" results from a result set (where the definition of "superfluous" is detector-
// specific).
CleanResults(results []Result) []Result
// ShouldCleanResultsIrrespectiveOfConfiguration allows a custom cleaner to instruct the engine to ignore
// user-provided configuration that controls whether results are cleaned. (User-provided configuration is not the
// only factor that determines whether the engine runs cleaning logic.)
ShouldCleanResultsIrrespectiveOfConfiguration() bool
}
// Versioner is an optional interface that a detector can implement to
// differentiate instances of the same detector type.
type Versioner interface {
Version() int
}
// MaxSecretSizeProvider is an optional interface that a detector can implement to
// provide a custom max size for the secret it finds.
type MaxSecretSizeProvider interface {
MaxSecretSize() int64
}
// StartOffsetProvider is an optional interface that a detector can implement to
// provide a custom start offset for the secret it finds.
type StartOffsetProvider interface {
StartOffset() int64
}
// MultiPartCredentialProvider is an optional interface that a detector can implement
// to indicate its compatibility with multi-part credentials and provide the maximum
// secret size for the credential it finds.
type MultiPartCredentialProvider interface {
// MaxCredentialSpan returns the maximum span or range of characters that the
// detector should consider when searching for a multi-part credential.
MaxCredentialSpan() int64
}
// EndpointCustomizer is an optional interface that a detector can implement to
// support verifying against user-supplied endpoints.
type EndpointCustomizer interface {
SetConfiguredEndpoints(...string) error
SetCloudEndpoint(string)
UseCloudEndpoint(bool)
UseFoundEndpoints(bool)
}
type CloudProvider interface {
CloudEndpoint() string
}
type Result struct {
// DetectorType is the type of Detector.
DetectorType detectorspb.DetectorType
// DetectorName is the name of the Detector. Used for custom detectors.
DetectorName string
// Verified indicates whether the result was verified or not.
Verified bool
// VerificationFromCache indicates whether this result's verification result came from the verification cache rather
// than an actual remote request.
VerificationFromCache bool
// Raw contains the raw secret identifier data. Prefer IDs over secrets since it is used for deduping after hashing.
Raw []byte
// RawV2 contains the raw secret identifier that is a combination of both the ID and the secret.
// This is used for secrets that are multi part and could have the same ID. Ex: AWS credentials
RawV2 []byte
// Redacted contains the redacted version of the raw secret identification data for display purposes.
// A secret ID should be used if available.
Redacted string
ExtraData map[string]string
StructuredData *detectorspb.StructuredData
// verificationError should be populated if the verification process itself failed in a way that provides no
// information about the verification status of the candidate secret, such as if the verification request timed out.
verificationError error
// AnalysisInfo should be set with information required for credential
// analysis to run. The keys of the map are analyzer specific and
// should match what is expected in the corresponding analyzer.
AnalysisInfo map[string]string
// primarySecret is used when a detector has multiple secret patterns.
// This secret is designated to determine the line number.
// If set, the line number will correspond to this secret.
primarySecret struct {
Value string
Line int64
}
}
// CopyVerificationInfo clones verification info (status and error) from another Result struct. This is used when
// loading verification info from a verification cache. (A method is necessary because verification errors are not
// exported, to prevent the accidental storage of sensitive information in them.)
func (r *Result) CopyVerificationInfo(from *Result) {
r.Verified = from.Verified
r.verificationError = from.verificationError
}
// SetVerificationError is the only way to set a new verification error. Any sensitive values should be passed-in as secrets to be redacted.
func (r *Result) SetVerificationError(err error, secrets ...string) {
if err != nil {
r.verificationError = redactSecrets(err, secrets...)
}
}
// Public accessors for the fields could also be provided if needed.
func (r *Result) VerificationError() error {
return r.verificationError
}
// SetPrimarySecretValue set the value passed as primary secret in the result
func (r *Result) SetPrimarySecretValue(value string) {
if value != "" {
r.primarySecret.Value = value
}
}
// SetPrimarySecretLine set the passed line number as primary secret line number
func (r *Result) SetPrimarySecretLine(line int64) {
// line number is only set if value is set for primary secret
if r.primarySecret.Value != "" {
r.primarySecret.Line = line
}
}
// GetPrimarySecretValue return primary secret match value
func (r *Result) GetPrimarySecretValue() string {
return r.primarySecret.Value
}
// redactSecrets replaces all instances of the given secrets with [REDACTED] in the error message.
func redactSecrets(err error, secrets ...string) error {
lastErr := unwrapToLast(err)
errStr := lastErr.Error()
for _, secret := range secrets {
errStr = strings.ReplaceAll(errStr, secret, "[REDACTED]")
}
return errors.New(errStr)
}
// unwrapToLast returns the last error in the chain of errors.
// This is added to exclude non-essential details (like URLs) for brevity and security.
// Also helps us optimize performance in redaction and enhance log clarity.
func unwrapToLast(err error) error {
for {
unwrapped := errors.Unwrap(err)
if unwrapped == nil {
// We've reached the last error in the chain
return err
}
err = unwrapped
}
}
type ResultWithMetadata struct {
// IsWordlistFalsePositive indicates whether this secret was flagged as a false positive based on a wordlist check
IsWordlistFalsePositive bool
// SourceMetadata contains source-specific contextual information.
SourceMetadata *source_metadatapb.MetaData
// SourceID is the ID of the source that the API uses to map secrets to specific sources.
SourceID sources.SourceID
// JobID is the ID of the job that the API uses to map secrets to specific jobs.
JobID sources.JobID
// SecretID is the ID of the secret, if it exists.
// Only secrets that are being reverified will have a SecretID.
SecretID int64
// SourceType is the type of Source.
SourceType sourcespb.SourceType
// SourceName is the name of the Source.
SourceName string
Result
// DetectorDescription is the description of the Detector.
DetectorDescription string
// DecoderType is the type of decoder that was used to generate this result's data.
DecoderType detectorspb.DecoderType
// ChunkData holds the original pre-decode source chunk data, preserved
// for secret storage encryption in the dispatcher.
ChunkData []byte
}
// CopyMetadata returns a detector result with included metadata from the source chunk.
func CopyMetadata(chunk *sources.Chunk, result Result) ResultWithMetadata {
// OriginalData may be nil when CopyMetadata is called outside the engine
// pipeline (e.g., in tests or external consumers that construct chunks directly).
chunkData := chunk.OriginalData
if chunkData == nil {
chunkData = chunk.Data
}
return ResultWithMetadata{
SourceMetadata: chunk.SourceMetadata,
SourceID: chunk.SourceID,
JobID: chunk.JobID,
SecretID: chunk.SecretID,
SourceType: chunk.SourceType,
SourceName: chunk.SourceName,
Result: result,
ChunkData: chunkData,
}
}
// CleanResults returns all verified secrets, and if there are no verified secrets,
// just one unverified secret if there are any.
func CleanResults(results []Result) []Result {
if len(results) == 0 {
return results
}
var cleaned = make(map[string]Result, 0)
for _, s := range results {
if s.Verified {
cleaned[s.Redacted] = s
}
}
if len(cleaned) == 0 {
return results[:1]
}
results = results[:0]
for _, r := range cleaned {
results = append(results, r)
}
return results
}
// PrefixRegex ensures that at least one of the given keywords is within
// 40 characters of the capturing group that follows.
// This can help prevent false positives.
func PrefixRegex(keywords []string) string {
pre := `(?i:`
middle := strings.Join(keywords, "|")
post := `)(?:.|[\n\r]){0,40}?`
return pre + middle + post
}
// KeyIsRandom is a Low cost check to make sure that 'keys' include a number to reduce FPs.
// Golang doesn't support regex lookaheads, so must be done in separate calls.
// TODO improve checks. Shannon entropy did not work well.
func KeyIsRandom(key string) bool {
for _, ch := range key {
if unicode.IsDigit(ch) {
return true
}
}
return false
}
func MustGetBenchmarkData() map[string][]byte {
sizes := map[string]int{
"xsmall": 10, // 10 bytes
"small": 100, // 100 bytes
"medium": 1024, // 1KB
"large": 10 * 1024, // 10KB
"xlarge": 100 * 1024, // 100KB
"xxlarge": 1024 * 1024, // 1MB
}
data := make(map[string][]byte)
for key, size := range sizes {
// Generating a byte slice of a specific size with random data.
content := make([]byte, size)
for i := range size {
randomByte, err := rand.Int(rand.Reader, big.NewInt(256))
if err != nil {
panic(err)
}
content[i] = byte(randomByte.Int64())
}
data[key] = content
}
return data
}
func RedactURL(u url.URL) string {
u.User = url.UserPassword(u.User.Username(), "********")
return strings.TrimSpace(strings.ReplaceAll(u.String(), "%2A", "*"))
}
func ParseURLAndStripPathAndParams(u string) (*url.URL, error) {
parsedURL, err := url.Parse(u)
if err != nil {
return nil, err
}
parsedURL.Path = ""
parsedURL.RawQuery = ""
return parsedURL, nil
}
================================================
FILE: pkg/detectors/detectors_test.go
================================================
//go:build detectors
// +build detectors
package detectors
import (
"testing"
regexp "github.com/wasilibs/go-re2"
)
func TestPrefixRegex(t *testing.T) {
tests := []struct {
keywords []string
expected string
}{
{
keywords: []string{"securitytrails"},
expected: `(?i:securitytrails)(?:.|[\n\r]){0,40}?`,
},
{
keywords: []string{"zipbooks"},
expected: `(?i:zipbooks)(?:.|[\n\r]){0,40}?`,
},
{
keywords: []string{"wrike"},
expected: `(?i:wrike)(?:.|[\n\r]){0,40}?`,
},
}
for _, tt := range tests {
got := PrefixRegex(tt.keywords)
if got != tt.expected {
t.Errorf("PrefixRegex(%v) got: %v want: %v", tt.keywords, got, tt.expected)
}
}
}
func TestPrefixRegexKeywords(t *testing.T) {
keywords := []string{"keyword1", "keyword2", "keyword3"}
testCases := []struct {
input string
expected bool
}{
{"keyword1 1234c4aabceeff4444442131444aab44", true},
{"keyword1 1234567890ABCDEF1234567890ABBBCA", false},
{"KEYWORD1 1234567890abcdef1234567890ababcd", true},
{"KEYWORD1 1234567890ABCDEF1234567890ABdaba", false},
{"keyword2 1234567890abcdef1234567890abeeff", true},
{"keyword2 1234567890ABCDEF1234567890ABadbd", false},
{"KEYWORD2 1234567890abcdef1234567890ababca", true},
{"KEYWORD2 1234567890ABCDEF1234567890ABBBBs", false},
{"keyword3 1234567890abcdef1234567890abccea", true},
{"KEYWORD3 1234567890abcdef1234567890abaabb", true},
{"keyword4 1234567890abcdef1234567890abzzzz", false},
{"keyword3 1234567890ABCDEF1234567890AB", false},
{"keyword4 1234567890ABCDEF1234567890AB", false},
}
keyPat := regexp.MustCompile(PrefixRegex(keywords) + `\b([0-9a-f]{32})\b`)
for _, tc := range testCases {
match := keyPat.MatchString(tc.input)
if match != tc.expected {
t.Errorf("Input: %s, Expected: %v, Got: %v", tc.input, tc.expected, match)
}
}
}
func BenchmarkPrefixRegex(b *testing.B) {
kws := []string{"securitytrails"}
for i := 0; i < b.N; i++ {
PrefixRegex(kws)
}
}
================================================
FILE: pkg/detectors/dfuse/dfuse.go
================================================
package dfuse
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(web\_[0-9a-z]{32})\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{"dfuse"}
}
// FromData will find and optionally verify Dfuse 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Dfuse,
Raw: []byte(resMatch),
}
if verify {
payload := strings.NewReader(`{"api_key":"` + resMatch + `"}`)
req, err := http.NewRequestWithContext(ctx, "POST", "https://auth.dfuse.io/v1/auth/issue", payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Dfuse
}
func (s Scanner) Description() string {
return "Dfuse is a blockchain API company providing access to blockchain data and infrastructure. Dfuse API keys can be used to access and interact with blockchain data."
}
================================================
FILE: pkg/detectors/dfuse/dfuse_integration_test.go
================================================
//go:build detectors
// +build detectors
package dfuse
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDfuse_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DFUSE")
inactiveSecret := testSecrets.MustGetField("DFUSE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dfuse secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dfuse,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dfuse secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dfuse,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Dfuse.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Dfuse.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/dfuse/dfuse_test.go
================================================
package dfuse
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
dfuse_secret: "web_akqaeqqsrlb5bczdblzgi4g94i3yt2jb"
base_url: "https://api.example.com/v1/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "web_akqaeqqsrlb5bczdblzgi4g94i3yt2jb"
)
func TestDfuse_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/diffbot/diffbot.go
================================================
package diffbot
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"diffbot"}) + `\b([a-z0-9]{32})\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{"diffbot"}
}
// FromData will find and optionally verify Diffbot 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Diffbot,
Raw: []byte(resMatch),
}
if verify {
timeout := 10 * time.Second
client.Timeout = timeout
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.diffbot.com/v4/account?token=%s", resMatch), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
bodyBytes, err := io.ReadAll(res.Body)
if err == nil {
bodyString := string(bodyBytes)
validResponse := strings.Contains(bodyString, `"token":`) && strings.Contains(bodyString, `"planCredits":`)
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
if validResponse {
s1.Verified = true
} else {
s1.Verified = false
}
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Diffbot
}
func (s Scanner) Description() string {
return "Diffbot is a service that provides APIs for extracting data from web pages. Diffbot API tokens can be used to access these services and extract data from web content."
}
================================================
FILE: pkg/detectors/diffbot/diffbot_integration_test.go
================================================
//go:build detectors
// +build detectors
package diffbot
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDiffbot_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DIFFBOT")
inactiveSecret := testSecrets.MustGetField("DIFFBOT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a diffbot secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Diffbot,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a diffbot secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Diffbot,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Diffbot.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Diffbot.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/diffbot/diffbot_test.go
================================================
package diffbot
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
diffbot_secret: "un7g0mse9r0i1m2p56832mja133vtysm"
base_url: "https://api.example.com/v1/example?token=$diffbot_secret"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "un7g0mse9r0i1m2p56832mja133vtysm"
)
func TestDiffBot_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/diggernaut/diggernaut.go
================================================
package diggernaut
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"diggernaut"}) + `\b([0-9a-z]{40})\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{"diggernaut"}
}
// FromData will find and optionally verify Diggernaut 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Diggernaut,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.diggernaut.com/api/projects", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Token %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Diggernaut
}
func (s Scanner) Description() string {
return "Diggernaut is a web scraping service. Diggernaut API keys can be used to access and manage scraping projects and data."
}
================================================
FILE: pkg/detectors/diggernaut/diggernaut_integration_test.go
================================================
//go:build detectors
// +build detectors
package diggernaut
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDiggernaut_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DIGGERNAUT")
inactiveSecret := testSecrets.MustGetField("DIGGERNAUT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a diggernaut secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Diggernaut,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a diggernaut secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Diggernaut,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Diggernaut.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Diggernaut.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/diggernaut/diggernaut_test.go
================================================
package diggernaut
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
diggernaut_secret: "vwrclry0t0ttuggr7gjdxarb9yb4td1618nziytp"
base_url: "https://api.example.com/v1/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "vwrclry0t0ttuggr7gjdxarb9yb4td1618nziytp"
)
func TestDiggerNaut_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/digitaloceantoken/digitaloceantoken.go
================================================
package digitaloceantoken
import (
"context"
"fmt"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ocean", "do"}) + `\b([A-Za-z0-9_-]{64})\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{"digitalocean"}
}
// FromData will find and optionally verify DigitalOceanToken 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)
var uniqueTokens = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueTokens[matches[1]] = struct{}{}
}
for token := range uniqueTokens {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_DigitalOceanToken,
Raw: []byte(token),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyDigitalOceanToken(ctx, client, token)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
if s1.Verified {
s1.AnalysisInfo = map[string]string{
"key": token,
}
}
}
results = append(results, s1)
}
return results, nil
}
func verifyDigitalOceanToken(ctx context.Context, client *http.Client, token string) (bool, error) {
// Ref: https://docs.digitalocean.com/reference/api/digitalocean/#tag/Account
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.digitalocean.com/v2/account", nil)
if err != nil {
return false, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
resp, err := client.Do(req)
if err != nil {
return false, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_DigitalOceanToken
}
func (s Scanner) Description() string {
return "DigitalOcean is a cloud infrastructure provider offering cloud services to help deploy, manage, and scale applications. DigitalOcean tokens can be used to access and manage these services."
}
================================================
FILE: pkg/detectors/digitaloceantoken/digitaloceantoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package digitaloceantoken
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/detectorspb"
)
func TestDigitalOceanToken_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DIGITALOCEAN_PERSONAL_ACCESS_TOKEN")
inactiveSecret := testSecrets.MustGetField("DIGITALOCEAN_PERSONAL_ACCESS_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a digitaloceantoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DigitalOceanToken,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a digitaloceantoken secret %s within but unverified", inactiveSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DigitalOceanToken,
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 verifiable secret, verification failed due to unexpected API response",
s: Scanner{client: common.ConstantResponseHttpClient(404, "")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a digitaloceantoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DigitalOceanToken,
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("DigitalOceanToken.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "AnalysisInfo")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("DigitalOceanToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/digitaloceantoken/digitaloceantoken_test.go
================================================
package digitaloceantoken
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
digitalocean_secret: "wisN3jbppF1dA3vrcB0C40iRlNiXAvEE8ToRFHkfBQS5dt5KIq-E8_vKW7NqrJJO"
base_url: "https://api.example.com/v1/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "wisN3jbppF1dA3vrcB0C40iRlNiXAvEE8ToRFHkfBQS5dt5KIq-E8_vKW7NqrJJO"
)
func TestDigitalOceanToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/digitaloceanv2/digitaloceanv2.go
================================================
package digitaloceanv2
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b((?:dop|doo|dor)_v1_[a-f0-9]{64})\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{"dop_v1_", "doo_v1_", "dor_v1_"}
}
// FromData will find and optionally verify DigitalOceanV2 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)
var uniqueTokens = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueTokens[matches[0]] = struct{}{}
}
for token := range uniqueTokens {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_DigitalOceanV2,
Raw: []byte(token),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
// Check if the token is a refresh token or an access token
switch {
case strings.HasPrefix(token, "dor_v1_"):
verified, verificationErr, newAccessToken := verifyRefreshToken(ctx, client, token)
s1.SetVerificationError(verificationErr)
s1.Verified = verified
if s1.Verified {
s1.AnalysisInfo = map[string]string{
"key": newAccessToken,
}
}
case strings.HasPrefix(token, "doo_v1_"), strings.HasPrefix(token, "dop_v1_"):
verified, verificationErr := verifyAccessToken(ctx, client, token)
s1.Verified = verified
s1.SetVerificationError(verificationErr)
if s1.Verified {
s1.AnalysisInfo = map[string]string{
"key": token,
}
}
}
}
results = append(results, s1)
}
return results, nil
}
// verifyRefreshToken verifies the refresh token by making a request to the DigitalOcean API.
// If the token is valid, it returns the new access token and no error.
// If the token is invalid/expired, it returns an empty string and no error.
// If an error is encountered, it returns an empty string along and the error.
func verifyRefreshToken(ctx context.Context, client *http.Client, token string) (bool, error, string) {
// Ref: https://docs.digitalocean.com/reference/api/oauth/
url := "https://cloud.digitalocean.com/v1/oauth/token?grant_type=refresh_token&refresh_token=" + token
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
if err != nil {
return false, fmt.Errorf("failed to create request: %w", err), ""
}
res, err := client.Do(req)
if err != nil {
return false, fmt.Errorf("failed to make request: %w", err), ""
}
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, fmt.Errorf("failed to read response body: %w", err), ""
}
defer res.Body.Close()
switch res.StatusCode {
case http.StatusOK:
var responseMap map[string]interface{}
if err := json.Unmarshal(bodyBytes, &responseMap); err != nil {
return false, fmt.Errorf("failed to parse response body: %w", err), ""
}
// Extract the access token from the response
accessToken, exists := responseMap["access_token"].(string)
if !exists {
return false, fmt.Errorf("access_token not found in response: %s", string(bodyBytes)), ""
}
return true, nil, accessToken
case http.StatusUnauthorized:
return false, nil, ""
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode), ""
}
}
// verifyAccessToken verifies the access token by making a request to the DigitalOcean API.
// If the token is valid, it returns true and no error.
// If the token is invalid, it returns false and no error.
// If an error is encountered, it returns false along with the error.
func verifyAccessToken(ctx context.Context, client *http.Client, token string) (bool, error) {
// Ref: https://docs.digitalocean.com/reference/api/digitalocean/#tag/Account
url := "https://api.digitalocean.com/v2/account"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return false, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
res, err := client.Do(req)
if err != nil {
return false, fmt.Errorf("failed to make request: %w", err)
}
defer res.Body.Close()
switch res.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_DigitalOceanV2
}
func (s Scanner) Description() string {
return "DigitalOcean is a cloud service provider offering scalable compute and storage solutions. DigitalOcean API keys can be used to access and manage these resources."
}
================================================
FILE: pkg/detectors/digitaloceanv2/digitaloceanv2_integration_test.go
================================================
//go:build detectors
// +build detectors
package digitaloceanv2
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/detectorspb"
)
func TestDigitalOceanV2_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DIGITALOCEANV2")
inactiveSecret := testSecrets.MustGetField("DIGITALOCEANV2_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a digitaloceanv2 secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DigitalOceanV2,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a digitaloceanv2 secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DigitalOceanV2,
Verified: false,
},
},
wantErr: 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,
},
{
name: "found verifiable secret, verification failed due to unexpected API response",
s: Scanner{client: common.ConstantResponseHttpClient(404, "")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a digitaloceanv2 secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DigitalOceanV2,
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("DigitalOceanV2.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "AnalysisInfo")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("DigitalOceanV2.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/digitaloceanv2/digitaloceanv2_test.go
================================================
package digitaloceanv2
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
digitalocean_secret1: "doo_v1_997deb722a2cf8f14a2eef30e41ed96e08268603b7877a595504e4367ca58e3d"
digitalocean_secret2: "dop_v1_997deb722a2cf8f14a2eef30e41ed96e08268603b7877a595504e4367ca58e3d"
digitalocean_secret3: "dor_v1_997deb722a2cf8f14a2eef30e41ed96e08268603b7877a595504e4367ca58e3d"
base_url: "https://api.example.com/v1/example?refresh_token=$digitalocean_secret1"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secrets = []string{
"doo_v1_997deb722a2cf8f14a2eef30e41ed96e08268603b7877a595504e4367ca58e3d",
"dop_v1_997deb722a2cf8f14a2eef30e41ed96e08268603b7877a595504e4367ca58e3d",
"dor_v1_997deb722a2cf8f14a2eef30e41ed96e08268603b7877a595504e4367ca58e3d",
}
)
func TestDigitalOceanV2_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: secrets,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/discordbottoken/discordbottoken.go
================================================
package discordbottoken
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"discord"}) + `\b([0-9]{17})\b`)
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"discord"}) + `\b([A-Za-z0-9_-]{24}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27})\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{"discord"}
}
// FromData will find and optionally verify DiscordBotToken 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatch := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idmatch := range idMatch {
resId := strings.TrimSpace(idmatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_DiscordBotToken,
Redacted: resId,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resId),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://discord.com/api/v8/users/"+resId, nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bot %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_DiscordBotToken
}
func (s Scanner) Description() string {
return "Discord bot tokens are used to authenticate and control Discord bots. These tokens can be used to interact with the Discord API to perform various bot-related operations."
}
================================================
FILE: pkg/detectors/discordbottoken/discordbottoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package discordbottoken
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDiscordBotToken_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DISCORDBOTTOKEN_TOKEN")
inactiveSecret := testSecrets.MustGetField("DISCORDBOTTOKEN_INACTIVE")
idSecret := testSecrets.MustGetField("DISCORDBOTTOKEN_USERID")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a discordbot secret %s within https://discord.com/api/v8/users/%s", secret, idSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DiscordBotToken,
Redacted: idSecret,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a discordbot secret %s within https://discord.com/api/v8/users/%s but not valid", inactiveSecret, idSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DiscordBotToken,
Redacted: idSecret,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("DiscordBotToken.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])
}
got[i].Raw = nil
if len(got[i].RawV2) == 0 {
t.Fatalf("no raw v2 secret present: \n %+v", got[i])
}
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("DiscordBotToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/discordbottoken/discordbottoken_test.go
================================================
package discordbottoken
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Token"
in: "Header"
discord_id: "17014529625858348"
discord_secret: "oHILWmk3qakMYbqAikD9R0nJ.Vhu0LY.FK1U_2L2Of8Bm5ESbD6Cy4VKu2K"
base_url: "https://api.example.com/v1/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "oHILWmk3qakMYbqAikD9R0nJ.Vhu0LY.FK1U_2L2Of8Bm5ESbD6Cy4VKu2K17014529625858348"
)
func TestDiscordBotToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/discordwebhook/discordwebhook.go
================================================
package discordwebhook
import (
"context"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = detectors.DetectorHttpClientWithNoLocalAddresses
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`(https:\/\/discord\.com\/api\/webhooks\/[0-9]{18,19}\/[0-9a-zA-Z-]{68})`)
)
// 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{"https://discord.com/api/webhooks/"} }
// FromData will find and optionally verify DiscordWebhook 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_DiscordWebhook,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, resMatch, nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_DiscordWebhook
}
func (s Scanner) Description() string {
return "Discord webhooks are used to send messages to a Discord channel. They can be used to automate messages and send data updates."
}
================================================
FILE: pkg/detectors/discordwebhook/discordwebhook_integration_test.go
================================================
//go:build detectors
// +build detectors
package discordwebhook
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDiscordWebhook_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DISCORDWEBHOOK")
inactiveSecret := testSecrets.MustGetField("DISCORDWEBHOOK_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a discordwebhook secret %s within discordwebhook", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DiscordWebhook,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a discordwebhook secret %s within discordwebhook but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DiscordWebhook,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("DiscordWebhook.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("DiscordWebhook.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/discordwebhook/discordwebhook_test.go
================================================
package discordwebhook
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
in: ""
discord_hook: "https://discord.com/api/webhooks/144147826297622273/Rz9B09dB7cXxtldzXXfmJY0opIzgeANtGJw08vx5PXrP8BpbOeE5lZ7wx8vVcyacYkEl"
base_url: ""
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "https://discord.com/api/webhooks/144147826297622273/Rz9B09dB7cXxtldzXXfmJY0opIzgeANtGJw08vx5PXrP8BpbOeE5lZ7wx8vVcyacYkEl"
validPattern19Digits = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
in: ""
discord_hook: "https://discord.com/api/webhooks/1369248176954937405/Q7bFGgbEMoZ-tRHuA4QHk3xTNC7nrrSmTm8IPjFvkp-ChRj4gi2C9lzvJiUcVlnE48X2"
base_url: ""
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret19Digits = "https://discord.com/api/webhooks/1369248176954937405/Q7bFGgbEMoZ-tRHuA4QHk3xTNC7nrrSmTm8IPjFvkp-ChRj4gi2C9lzvJiUcVlnE48X2"
)
func TestDiscordWebHook_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
{
name: "valid pattern with 19-digit ID",
input: validPattern19Digits,
want: []string{secret19Digits},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/disqus/disqus.go
================================================
package disqus
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"disqus"}) + `\b([a-zA-Z0-9]{64})\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{"disqus"}
}
// FromData will find and optionally verify Disqus 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Disqus,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://disqus.com/api/3.0/trends/listThreads.json?api_key="+resMatch, nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Disqus
}
func (s Scanner) Description() string {
return "Disqus is a networked community platform used for web comments and discussions. Disqus API keys can be used to access and manage comments and user data."
}
================================================
FILE: pkg/detectors/disqus/disqus_integration_test.go
================================================
//go:build detectors
// +build detectors
package disqus
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDisqus_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DISQUS")
inactiveSecret := testSecrets.MustGetField("DISQUS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a disqus secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Disqus,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a disqus secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Disqus,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Disqus.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Disqus.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/disqus/disqus_test.go
================================================
package disqus
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
base_url: "https://api.disqus.com/v3/example?token=T7YaiuviPyYp8WyWlJ9lqQLI5oPirYMcfDYLPY7NAqxAr3872ovqq9AOVU3RcPUB"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "T7YaiuviPyYp8WyWlJ9lqQLI5oPirYMcfDYLPY7NAqxAr3872ovqq9AOVU3RcPUB"
)
func TestDisqus_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/ditto/ditto.go
================================================
package ditto
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ditto"}) + `\b([a-z0-9]{8}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{12}\.[a-z0-9]{40})\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{"ditto"}
}
// FromData will find and optionally verify Ditto 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Ditto,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.dittowords.com/variants", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("token %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Ditto
}
func (s Scanner) Description() string {
return "Ditto is a service that provides API access to various word variants. Ditto API keys can be used to access this service and retrieve word variants."
}
================================================
FILE: pkg/detectors/ditto/ditto_integration_test.go
================================================
//go:build detectors
// +build detectors
package ditto
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDitto_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DITTO")
inactiveSecret := testSecrets.MustGetField("DITTO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a ditto secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Ditto,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a ditto secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Ditto,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Ditto.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Ditto.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/ditto/ditto_test.go
================================================
package ditto
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
api_version: v1
ditto_secret: "smtkww1b-bpux-6mds-r977-7kr1rb1q8r5o.4jwv35awjadnwzzm4u4kz8otf3lgmns2oazb8f6w"
base_url: "https://api.ditto.com/$api_version/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "smtkww1b-bpux-6mds-r977-7kr1rb1q8r5o.4jwv35awjadnwzzm4u4kz8otf3lgmns2oazb8f6w"
)
func TestDitto_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/dnscheck/dnscheck.go
================================================
package dnscheck
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"dnscheck"}) + `\b([a-z0-9A-Z]{32})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"dnscheck"}) + `\b([a-z0-9A-Z-]{36})\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{"dnscheck"}
}
// FromData will find and optionally verify Dnscheck 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idmatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idmatch := range idmatches {
resIdMatch := strings.TrimSpace(idmatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Dnscheck,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.dnscheck.co/api/v1/groups/"+resIdMatch+"?api_key="+resMatch, nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Dnscheck
}
func (s Scanner) Description() string {
return "Dnscheck is a service used to monitor DNS records. The API keys can be used to access and manage DNS monitoring configurations."
}
================================================
FILE: pkg/detectors/dnscheck/dnscheck_integration_test.go
================================================
//go:build detectors
// +build detectors
package dnscheck
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDnscheck_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DNSCHECK")
inactiveSecret := testSecrets.MustGetField("DNSCHECK_INACTIVE")
id := testSecrets.MustGetField("DNSCHECK_ID")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dnscheck secret %s within dnscheckid %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dnscheck,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dnscheck secret %s within dnscheckid %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dnscheck,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Dnscheck.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])
}
got[i].Raw = nil
if len(got[i].RawV2) == 0 {
t.Fatalf("no raw v2 secret present: \n %+v", got[i])
}
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Dnscheck.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/dnscheck/dnscheck_test.go
================================================
package dnscheck
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
api_version: v1
dnscheck_secret: "GaMSE8mJT7evjXg1Tmwz0wAyrY4Yagur"
base_url: "https://api.dnscheck.com/$api_version/groups/zDTON8dac54pwe1OaCrKhcwC9qptimIdX42K?api_key=$dnscheck_secret"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "GaMSE8mJT7evjXg1Tmwz0wAyrY4YagurzDTON8dac54pwe1OaCrKhcwC9qptimIdX42K"
)
func TestDnsCheck_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/docker/docker_auth_config.go
================================================
package docker
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/go-logr/logr"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ interface {
detectors.Detector
detectors.MaxSecretSizeProvider
} = (*Scanner)(nil)
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Docker
}
func (s Scanner) Description() string {
return "Docker credentials can be used to pull images from private registries."
}
// 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{`"auths"`, `\"auths\`}
}
func (s Scanner) MaxSecretSize() int64 {
return 4096
}
var (
keyPat = regexp.MustCompile(`{(?:\s|\\+[nrt])*\\*"auths\\*"(?:\s|\\+t)*:(?:\s|\\+t)*{(?:\s|\\+[nrt])*\\*"(?i:https?:\/\/)?[a-z0-9\-.:\/]+\\*"(?:\s|\\+t)*:(?:\s|\\+t)*{(?:(?:\s|\\+[nrt])*\\*"(?i:auth|email|username|password)\\*"\s*:\s*\\*".*\\*"\s*,?)+?(?:\s|\\+[nrt])*}(?:\s|\\+[nrt])*}(?:\s|\\+[nrt])*}`)
escapedReplacer = strings.NewReplacer(
`\n`, "",
`\r`, "",
`\t`, "",
`\\`, ``,
`\"`, `"`,
)
// Common false-positives used in examples.
exampleRegistries = map[string]struct{}{
"https://index.docker.io/v1/": {}, // https://github.com/moby/moby/blob/34679e568a22b4f35ff8460f3b5b7bf7089df818/cliconfig/config_test.go#L259
"registry.hostname.com": {}, // https://github.com/openshift/machine-config-operator/blob/82011335dbdd3d4c869b959d6048a3fba7742e47/pkg/controller/build/helpers_test.go#L47
"registry.example.com:5000": {}, // https://github.com/openshift/cluster-baremetal-operator/blob/f908020b1d46667056f21cf1d79e032c535a41fc/provisioning/baremetal_secrets_test.go#L53
"registry2.example.com:5000": {},
"your.private.registry.example.com": {}, // https://github.com/kubernetes/website/blob/d130f326758988553c42179c087bfeec5bf948a0/content/en/docs/tasks/configure-pod-container/pull-image-private-registry.md?plain=1#L167
}
)
// FromData will find and optionally verify Docker 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)
logCtx := logContext.AddLogger(ctx)
logger := logCtx.Logger().WithName("docker")
uniqueMatches := make(map[string]struct{})
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueMatches[match[0]] = struct{}{}
}
for match := range uniqueMatches {
// Remove escaped quotes and literal whitespace characters, if present.
// It is common for auth to be escaped, however, the json package cannot unmarshal escaped JSON.
match := escapedReplacer.Replace(match)
// Unmarshal the config string.
// Doing byte->string->byte probably isn't the most efficient.
var auths dockerAuths
if err := json.NewDecoder(strings.NewReader(match)).Decode(&auths); err != nil {
logger.Error(err, "Could not parse Docker auth JSON")
return results, err
} else if len(auths.Auths) == 0 {
continue
}
for registry, auth := range auths.Auths {
// `docker.io` is a special case, Docker is hard-coded to rewrite it as `index.docker.io`.
// https://github.com/moby/moby/blob/145a73a36c171b34c196ad780e699b154ddf47b5/registry/config_test.go#L329
if strings.EqualFold(registry, "docker.io") {
registry = "index.docker.io"
}
// Skip known invalid registries.
if _, ok := exampleRegistries[registry]; ok {
continue
}
// Skip configs with no credentials.
// TODO: Should this be an error? What if it's a logic issue?
username, password, b64encoded := parseBasicAuth(logger, auth)
if username == "" && password == "" {
logger.V(2).Info("Skipping empty credentials", "auth", auth, "username", username, "password", password)
continue
}
r := detectors.Result{
DetectorType: detectorspb.DetectorType_Docker,
Raw: []byte(b64encoded),
RawV2: []byte(`{"registry":"` + registry + `","auth":"` + b64encoded + `"}`),
ExtraData: map[string]string{"Username": username},
}
if verify {
client := s.client
if client == nil {
client = common.SaneHttpClient()
}
isVerified, verificationErr := verifyMatch(logCtx, client, registry, username, b64encoded)
r.Verified = isVerified
r.SetVerificationError(verificationErr, match)
}
results = append(results, r)
}
}
return
}
func verifyMatch(ctx logContext.Context, client *http.Client, registry string, username string, basicAuth string) (bool, error) {
// Build the registry URL path.
var registryUrl string
registry, _ = strings.CutSuffix(registry, "/")
if strings.HasPrefix(registry, "http://") || strings.HasPrefix(registry, "https://") {
registryUrl = registry + "/v2/"
} else {
registryUrl = "https://" + registry + "/v2/"
}
// Build the request.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, registryUrl, nil)
if err != nil {
return false, err
}
req.Header.Set("Authorization", "Basic "+basicAuth)
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
// Send the initial request.
res, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
// Handle the initial response.
switch res.StatusCode {
case http.StatusOK:
body, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
return json.Valid(body), nil
case http.StatusUnauthorized:
// Some registries do not support basic auth, so we must follow the `Www-Authenticate` header, if present.
// https://distribution.github.io/distribution/spec/auth/token/
h := res.Header.Get("Www-Authenticate")
if h == "" {
return false, nil
}
if !strings.HasPrefix(h, "Bearer") {
return false, fmt.Errorf("unsupported WWW-Authenticate auth scheme: %s", h)
}
authParams, err := parseAuthenticateHeader(h)
if err != nil {
return false, fmt.Errorf("failed to parse registry auth header: %w", err)
}
realm := authParams["realm"]
if realm == "" {
return false, fmt.Errorf("unexpected empty realm for WWW-Authenticate header: %s", h)
}
authReq, err := http.NewRequestWithContext(ctx, http.MethodGet, realm, nil)
if err != nil {
return false, nil
}
authReq.Header.Set("Authorization", "Basic "+basicAuth)
authReq.Header.Set("Accept", "application/json")
authReq.Header.Set("Content-Type", "application/json")
params := url.Values{}
params.Add("account", username)
params.Add("service", authParams["service"])
authReq.URL.RawQuery = params.Encode()
authRes, err := client.Do(authReq)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, authRes.Body)
_ = authRes.Body.Close()
}()
switch authRes.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized, http.StatusForbidden:
// Auth was rejected.
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d for '%s'", authRes.StatusCode, authReq.URL.String())
}
default:
err = fmt.Errorf("unexpected HTTP response status %d for '%s'", res.StatusCode, req.URL.String())
return false, err
}
}
type dockerAuths struct {
Auths map[string]dockerAuth `json:"auths"`
}
type dockerAuth struct {
Auth string `json:"auth"`
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
}
// parseBasicAuth handles cases where configs can have `username` and `password` but no `auth`,
// or vice-versa.
func parseBasicAuth(logger logr.Logger, auth dockerAuth) (string, string, string) {
var (
username string
password string
)
if auth.Username != "" && auth.Password != "" {
username = auth.Username
password = auth.Password
}
if auth.Auth != "" {
data, err := base64.StdEncoding.DecodeString(auth.Auth)
if err != nil {
goto end
}
parts := strings.SplitN(string(data), ":", 2)
if len(parts) != 2 {
logger.V(2).Info("Skipping invalid parts", "length", len(parts), "parts", parts)
goto end
}
if (username != "" && parts[0] != username) || (password != "" && parts[1] != password) {
logger.V(2).Info("WARNING: Creds have more than two usernames or passwords")
}
username = parts[0]
password = parts[1]
}
end:
if username == "" && password == "" {
return "", "", ""
}
basicAuth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
if auth.Auth != "" && basicAuth != auth.Auth {
logger.Error(fmt.Errorf("base64-encoded auth does not match source"), "failed to parse auths JSON")
}
return username, password, basicAuth
}
// This is an ad-hoc implementation and not RFC compliant.
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate
func parseAuthenticateHeader(headerValue string) (map[string]string, error) {
authParams := make(map[string]string)
parts := strings.Split(headerValue, " ")
if len(parts) < 2 {
return nil, fmt.Errorf("invalid WWW-Authenticate header format")
}
authParams["scheme"] = parts[0]
parts = strings.Split(parts[1], ",")
for _, part := range parts {
keyVal := strings.SplitN(strings.TrimSpace(part), "=", 2)
if len(keyVal) == 2 {
key := strings.TrimSpace(keyVal[0])
value := strings.Trim(strings.TrimSpace(keyVal[1]), `"`)
authParams[key] = value
}
}
return authParams, nil
}
================================================
FILE: pkg/detectors/docker/docker_auth_config_integration_test.go
================================================
//go:build detectors
// +build detectors
package docker
func TestDocker_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("DOCKER")
inactiveSecret := testSecrets.MustGetField("DOCKER_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a docker secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Docker,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a docker secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Docker,
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 docker secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Docker,
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 docker secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Docker,
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("Docker.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Docker.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/docker/docker_auth_config_test.go
================================================
package docker
import (
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
"testing"
)
func TestDocker_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
// Kubernetes public test credentials
// https://github.com/kubernetes/autoscaler/blob/f22b40eab867cbc52bdb15dc8768962e21d22837/vertical-pod-autoscaler/e2e/vendor/k8s.io/kubernetes/test/e2e/common/node/runtime.go#L283C1-L290C2
{
name: "GCP auth",
input: `{
"auths": {
"https://gcr.io": {
"auth": "X2pzb25fa2V5OnsKICAidHlwZSI6ICJzZXJ2aWNlX2FjY291bnQiLAogICJwcm9qZWN0X2lkIjogImF1dGhlbnRpY2F0ZWQtaW1hZ2UtcHVsbGluZyIsCiAgInByaXZhdGVfa2V5X2lkIjogImI5ZjJhNjY0YWE5YjIwNDg0Y2MxNTg2MDYzZmVmZGExOTIyNGFjM2IiLAogICJwcml2YXRlX2tleSI6ICItLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS1cbk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQzdTSG5LVEVFaVlMamZcbkpmQVBHbUozd3JCY2VJNTBKS0xxS21GWE5RL3REWGJRK2g5YVl4aldJTDhEeDBKZTc0bVovS01uV2dYRjVLWlNcbm9BNktuSU85Yi9SY1NlV2VpSXRSekkzL1lYVitPNkNjcmpKSXl4anFWam5mVzJpM3NhMzd0OUE5VEZkbGZycm5cbjR6UkpiOWl4eU1YNGJMdHFGR3ZCMDNOSWl0QTNzVlo1ODhrb1FBZmgzSmhhQmVnTWorWjRSYko0aGVpQlFUMDNcbnZVbzViRWFQZVQ5RE16bHdzZWFQV2dydDZOME9VRGNBRTl4bGNJek11MjUzUG4vSzgySFpydEx4akd2UkhNVXhcbng0ZjhwSnhmQ3h4QlN3Z1NORit3OWpkbXR2b0wwRmE3ZGducFJlODZWRDY2ejNZenJqNHlLRXRqc2hLZHl5VWRcbkl5cVhoN1JSQWdNQkFBRUNnZ0VBT3pzZHdaeENVVlFUeEFka2wvSTVTRFVidi9NazRwaWZxYjJEa2FnbmhFcG9cbjFJajJsNGlWMTByOS9uenJnY2p5VlBBd3pZWk1JeDFBZVF0RDdoUzRHWmFweXZKWUc3NkZpWFpQUm9DVlB6b3VcbmZyOGRDaWFwbDV0enJDOWx2QXNHd29DTTdJWVRjZmNWdDdjRTEyRDNRS3NGNlo3QjJ6ZmdLS251WVBmK0NFNlRcbmNNMHkwaCtYRS9kMERvSERoVy96YU1yWEhqOFRvd2V1eXRrYmJzNGYvOUZqOVBuU2dET1lQd2xhbFZUcitGUWFcbkpSd1ZqVmxYcEZBUW14M0Jyd25rWnQzQ2lXV2lGM2QrSGk5RXRVYnRWclcxYjZnK1JRT0licWFtcis4YlJuZFhcbjZWZ3FCQWtKWjhSVnlkeFVQMGQxMUdqdU9QRHhCbkhCbmM0UW9rSXJFUUtCZ1FEMUNlaWN1ZGhXdGc0K2dTeGJcbnplanh0VjFONDFtZHVjQnpvMmp5b1dHbzNQVDh3ckJPL3lRRTM0cU9WSi9pZCs4SThoWjRvSWh1K0pBMDBzNmdcblRuSXErdi9kL1RFalk4MW5rWmlDa21SUFdiWHhhWXR4UjIxS1BYckxOTlFKS2ttOHRkeVh5UHFsOE1veUdmQ1dcbjJ2aVBKS05iNkhabnY5Q3lqZEo5ZzJMRG5RS0JnUUREcVN2eURtaGViOTIzSW96NGxlZ01SK205Z2xYVWdTS2dcbkVzZlllbVJmbU5XQitDN3ZhSXlVUm1ZNU55TXhmQlZXc3dXRldLYXhjK0krYnFzZmx6elZZdFpwMThNR2pzTURcbmZlZWZBWDZCWk1zVXQ3Qmw3WjlWSjg1bnRFZHFBQ0xwWitaLzN0SVJWdWdDV1pRMWhrbmxHa0dUMDI0SkVFKytcbk55SDFnM2QzUlFLQmdRQ1J2MXdKWkkwbVBsRklva0tGTkh1YTBUcDNLb1JTU1hzTURTVk9NK2xIckcxWHJtRjZcbkMwNGNTKzQ0N0dMUkxHOFVUaEpKbTRxckh0Ti9aK2dZOTYvMm1xYjRIakpORDM3TVhKQnZFYTN5ZUxTOHEvK1JcbjJGOU1LamRRaU5LWnhQcG84VzhOSlREWTVOa1BaZGh4a2pzSHdVNGRTNjZwMVRESUU0MGd0TFpaRFFLQmdGaldcbktyblFpTnEzOS9iNm5QOFJNVGJDUUFKbmR3anhTUU5kQTVmcW1rQTlhRk9HbCtqamsxQ1BWa0tNSWxLSmdEYkpcbk9heDl2OUc2Ui9NSTFIR1hmV3QxWU56VnRocjRIdHNyQTB0U3BsbWhwZ05XRTZWejZuQURqdGZQSnMyZUdqdlhcbmpQUnArdjhjY21MK3dTZzhQTGprM3ZsN2VlNXJsWWxNQndNdUdjUHhBb0dBZWRueGJXMVJMbVZubEFpSEx1L0xcbmxtZkF3RFdtRWlJMFVnK1BMbm9Pdk81dFE1ZDRXMS94RU44bFA0cWtzcGtmZk1Rbk5oNFNZR0VlQlQzMlpxQ1RcbkpSZ2YwWGpveXZ2dXA5eFhqTWtYcnBZL3ljMXpmcVRaQzBNTzkvMVVjMWJSR2RaMmR5M2xSNU5XYXA3T1h5Zk9cblBQcE5Gb1BUWGd2M3FDcW5sTEhyR3pNPVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwKICAiY2xpZW50X2VtYWlsIjogImltYWdlLXB1bGxpbmdAYXV0aGVudGljYXRlZC1pbWFnZS1wdWxsaW5nLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwKICAiY2xpZW50X2lkIjogIjExMzc5NzkxNDUzMDA3MzI3ODcxMiIsCiAgImF1dGhfdXJpIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi9hdXRoIiwKICAidG9rZW5fdXJpIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsCiAgImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjEvY2VydHMiLAogICJjbGllbnRfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9yb2JvdC92MS9tZXRhZGF0YS94NTA5L2ltYWdlLXB1bGxpbmclNDBhdXRoZW50aWNhdGVkLWltYWdlLXB1bGxpbmcuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iCn0=",
"email": "image-pulling@authenticated-image-pulling.iam.gserviceaccount.com"
}
}
}`,
want: []string{`{"registry":"https://gcr.io","auth":"X2pzb25fa2V5OnsKICAidHlwZSI6ICJzZXJ2aWNlX2FjY291bnQiLAogICJwcm9qZWN0X2lkIjogImF1dGhlbnRpY2F0ZWQtaW1hZ2UtcHVsbGluZyIsCiAgInByaXZhdGVfa2V5X2lkIjogImI5ZjJhNjY0YWE5YjIwNDg0Y2MxNTg2MDYzZmVmZGExOTIyNGFjM2IiLAogICJwcml2YXRlX2tleSI6ICItLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS1cbk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQzdTSG5LVEVFaVlMamZcbkpmQVBHbUozd3JCY2VJNTBKS0xxS21GWE5RL3REWGJRK2g5YVl4aldJTDhEeDBKZTc0bVovS01uV2dYRjVLWlNcbm9BNktuSU85Yi9SY1NlV2VpSXRSekkzL1lYVitPNkNjcmpKSXl4anFWam5mVzJpM3NhMzd0OUE5VEZkbGZycm5cbjR6UkpiOWl4eU1YNGJMdHFGR3ZCMDNOSWl0QTNzVlo1ODhrb1FBZmgzSmhhQmVnTWorWjRSYko0aGVpQlFUMDNcbnZVbzViRWFQZVQ5RE16bHdzZWFQV2dydDZOME9VRGNBRTl4bGNJek11MjUzUG4vSzgySFpydEx4akd2UkhNVXhcbng0ZjhwSnhmQ3h4QlN3Z1NORit3OWpkbXR2b0wwRmE3ZGducFJlODZWRDY2ejNZenJqNHlLRXRqc2hLZHl5VWRcbkl5cVhoN1JSQWdNQkFBRUNnZ0VBT3pzZHdaeENVVlFUeEFka2wvSTVTRFVidi9NazRwaWZxYjJEa2FnbmhFcG9cbjFJajJsNGlWMTByOS9uenJnY2p5VlBBd3pZWk1JeDFBZVF0RDdoUzRHWmFweXZKWUc3NkZpWFpQUm9DVlB6b3VcbmZyOGRDaWFwbDV0enJDOWx2QXNHd29DTTdJWVRjZmNWdDdjRTEyRDNRS3NGNlo3QjJ6ZmdLS251WVBmK0NFNlRcbmNNMHkwaCtYRS9kMERvSERoVy96YU1yWEhqOFRvd2V1eXRrYmJzNGYvOUZqOVBuU2dET1lQd2xhbFZUcitGUWFcbkpSd1ZqVmxYcEZBUW14M0Jyd25rWnQzQ2lXV2lGM2QrSGk5RXRVYnRWclcxYjZnK1JRT0licWFtcis4YlJuZFhcbjZWZ3FCQWtKWjhSVnlkeFVQMGQxMUdqdU9QRHhCbkhCbmM0UW9rSXJFUUtCZ1FEMUNlaWN1ZGhXdGc0K2dTeGJcbnplanh0VjFONDFtZHVjQnpvMmp5b1dHbzNQVDh3ckJPL3lRRTM0cU9WSi9pZCs4SThoWjRvSWh1K0pBMDBzNmdcblRuSXErdi9kL1RFalk4MW5rWmlDa21SUFdiWHhhWXR4UjIxS1BYckxOTlFKS2ttOHRkeVh5UHFsOE1veUdmQ1dcbjJ2aVBKS05iNkhabnY5Q3lqZEo5ZzJMRG5RS0JnUUREcVN2eURtaGViOTIzSW96NGxlZ01SK205Z2xYVWdTS2dcbkVzZlllbVJmbU5XQitDN3ZhSXlVUm1ZNU55TXhmQlZXc3dXRldLYXhjK0krYnFzZmx6elZZdFpwMThNR2pzTURcbmZlZWZBWDZCWk1zVXQ3Qmw3WjlWSjg1bnRFZHFBQ0xwWitaLzN0SVJWdWdDV1pRMWhrbmxHa0dUMDI0SkVFKytcbk55SDFnM2QzUlFLQmdRQ1J2MXdKWkkwbVBsRklva0tGTkh1YTBUcDNLb1JTU1hzTURTVk9NK2xIckcxWHJtRjZcbkMwNGNTKzQ0N0dMUkxHOFVUaEpKbTRxckh0Ti9aK2dZOTYvMm1xYjRIakpORDM3TVhKQnZFYTN5ZUxTOHEvK1JcbjJGOU1LamRRaU5LWnhQcG84VzhOSlREWTVOa1BaZGh4a2pzSHdVNGRTNjZwMVRESUU0MGd0TFpaRFFLQmdGaldcbktyblFpTnEzOS9iNm5QOFJNVGJDUUFKbmR3anhTUU5kQTVmcW1rQTlhRk9HbCtqamsxQ1BWa0tNSWxLSmdEYkpcbk9heDl2OUc2Ui9NSTFIR1hmV3QxWU56VnRocjRIdHNyQTB0U3BsbWhwZ05XRTZWejZuQURqdGZQSnMyZUdqdlhcbmpQUnArdjhjY21MK3dTZzhQTGprM3ZsN2VlNXJsWWxNQndNdUdjUHhBb0dBZWRueGJXMVJMbVZubEFpSEx1L0xcbmxtZkF3RFdtRWlJMFVnK1BMbm9Pdk81dFE1ZDRXMS94RU44bFA0cWtzcGtmZk1Rbk5oNFNZR0VlQlQzMlpxQ1RcbkpSZ2YwWGpveXZ2dXA5eFhqTWtYcnBZL3ljMXpmcVRaQzBNTzkvMVVjMWJSR2RaMmR5M2xSNU5XYXA3T1h5Zk9cblBQcE5Gb1BUWGd2M3FDcW5sTEhyR3pNPVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwKICAiY2xpZW50X2VtYWlsIjogImltYWdlLXB1bGxpbmdAYXV0aGVudGljYXRlZC1pbWFnZS1wdWxsaW5nLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwKICAiY2xpZW50X2lkIjogIjExMzc5NzkxNDUzMDA3MzI3ODcxMiIsCiAgImF1dGhfdXJpIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi9hdXRoIiwKICAidG9rZW5fdXJpIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsCiAgImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjEvY2VydHMiLAogICJjbGllbnRfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9yb2JvdC92MS9tZXRhZGF0YS94NTA5L2ltYWdlLXB1bGxpbmclNDBhdXRoZW50aWNhdGVkLWltYWdlLXB1bGxpbmcuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iCn0="}`},
},
// Relies on the base64 decoder, which isn't present in this test (yet?)
// {
// name: "kubernetes .dockerconfigjson",
// input: `apiVersion: v1
//data:
// .dockerconfigjson: eyJhdXRocyI6eyJodHRwczovL2djci5pbyI6eyJ1c2VybmFtZSI6Il9qc29uX2tleSIsInBhc3N3b3JkIjoie1xuICBcInR5cGVcIjogXCJzZXJ2aWNlX2FjY291bnRcIixcbiAgXCJwcm9qZWN0X2lkXCI6IFwiY29uc3RhbnQtY3ViaXN0LTE3MzEyM1wiLFxuICBcInByaXZhdGVfa2V5X2lkXCI6IFwiYWRiMzY3M2NiOTkzNzkyNjZiY2MxZDU1YmIxZTdiZDFlYzM5NGI1Y1wiLFxuICBcInByaXZhdGVfa2V5XCI6IFwiLS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tXFxuTUlJRXZRSUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NCS2N3Z2dTakFnRUFBb0lCQVFDNm8zN0o4S2kxUWp3RVxcbnhNT3ROUVZaK2xsWUxIdlNXV2tDeXp1a3JwbHdZRU9KRk5VR00yQ3NySHpjM0pDUDhGYWo1RVRHMjlvT1pLVkJcXG5MSjU3eVdKSEpyekhIb2JyOHNsNytpcjRjYUovSzNiS2lybmZWYTZFeXk5azFIa0RMSlZ4T1lsaXFTbkdtRlZ5XFxuQ3lpYXltNTI1V3VqanZIQkRaZUdsYzlqb1RLMG9yQXYvUCthZzhleUUvY05DS0FwTkk4ZTFXYmlhMFNCdWEwblxcblVZbFB1RXRxdzJ3NDhJbkh6akVQY0VmdENzWjBOZGhkY3hTdVNuSVB5NW9ua2JuVXhZWnAzUjF3TmQ3eDdaQk5cXG5ESmFCWEJTMlVkR1M0ditzeWJVQlU1aXFBckRNbVNmWUwxN09TU3ZzdEZSdVEydkJaa0M3TU96RWd2MUlIZjBXXFxubzlOSzBFaHZBZ01CQUFFQ2dnRUFDUm1MbkZaSzJORHFrdU9kRkJ3dnIwYTdoY2NLeW5pOWhxWURaSERNTTduSVxcbmU5aUkxN2ZpNWgyMWdNeVM1OUcwc21KTGV0UDJwUmtCemFtdjdjMGwwNGp2VDFpM3IxZ0pFWU1Oc1V0VHZFRG1cXG42OUorWkRDTjc3K1FYS21DQ2tZKzRHUmVieHhjV0doNC9MUjZrd0Y5Qi9oV1JTL2xBdlZNc1ZmVjRyK3JTZVNjXFxubU1KOTRBUTROM3hyV0VRc3Vpd1ZIZldMdElMTWZGN1JoV3VzdjJiZ1gvRCs0ajdISHRoODVrYlcxSzR0MnFkN1xcbkIwaEJEcVlQTEtjYzJVNkNJR0NRZ1h3THNlYUUxRkptYWpsdnNVK0pXdmY2MmZTNk8wSlVMeVFLMzZkczZkRlJcXG5qaDg1TWJsZVlHMWdpaFpPTXJtcENvWklUazdFT01lU2pQZ0VaWG5pVVFLQmdRRDNtUHJrZmVKVWNXWmpNZndCXFxuYnJJNE1NRWl2R1JJVDR5RzZxZHZpZFIrd1U3djMxbG1Oa1A3S2s4K1hoQWtGMk1pQkRSakFGVm8rNHQ3a3paRFxcbk45Zk9NSlgwakZnUVpLajFuR1gxekZ2ZERqRTh3ZWRTN2ZMV3BOczJlZm5GWUpQRm5SRU16eG81VWpGNTZ4V1pcXG5ZQmI2VHlNaTNRa1lEblA0WTVjTUZCVUpiUUtCZ1FEQStPNDlUc2EyYWdWUnJYbC8xRDNzWHd0UjdKSGhuRURkXFxuNWlZM0FtOVQxV2pVVTE1T2lwTUxOaXpBb3lRWGlKRVlaMmNuRHZnbHdkNEsvMWFLbityc3hzWUdFZjhoWVBIclxcbkJoN3FueW44SzJseTJoakUxY0xpVFg4NEVnd1VMcFJjeGo3bkM0ZWFLOEdJeUdLNnZrR3NoNCs1bnJLVFlkaUtcXG5MeUhSMUc2cnl3S0JnUURnLzJqSGFNbmEySzRsYUUvTWNXNk05MmtiQ3IzS3BGZGNaeksrZmk3Vy9RMmhsNEtqXFxuQ3A4ZVNDVjQxSHV3Z0h3NmRqMncxYVhINEFheHhtWWlFVVlQL2tEVzJRNVIzMWRXMHNnbzVJdDZSeUpoUndmU1xcbmFaOHFoT2NjQ3gzNXlqaWU5SXVBNjFhMlRrWGR0ODZKOFRNUVJnZjA3NDRMQ1Y5RGtpUzUraW5meFFLQmdFMVdcXG5ObHlacXFmR203VWRPZmxSL1RNeThCMTRHd3I1RFVJaEQ2V3lNeDI5QkpNN2lpc2QvRXBjL3RpQlNXQ3BHY1ZYXFxuQTQ4eXY1NmFNTHZsa3pCaFlNeGQ2VlRiZDQxUUJnUXo0c1lTM2Nlek9rS09SNmp6Sm5SOXJJT3pMK1lTdU9EcFxcbmpxSVlDOU5zdjlacXdLNm91emRDNlFYeUpRMU9CSE4wNmkvbTNDZTdBb0dBU01wRStscDlxV2ZWYXlGV2tlWVBcXG5OOFhId2FNUWNkT0ZkbDZFdlF0ZWtQY0xiQ1F6UzRSdEhBT01NTDN5ci9DQUk5SmZkanhWMHdicW1oNlJ3WFAzXFxuKzhkOVJpNjhsMGV3NUhLMDJWRHFhZE8vOTJhaHNrNmYxV1ZOL0dMcFg4Yk9NZEZFdnJOS09zUVk0RW9DV0JTa1xcblF1ZmRBdFZueE1UZG9ydTNxY0N4RG1vPVxcbi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS1cXG5cIixcbiAgXCJjbGllbnRfZW1haWxcIjogXCJrZi1hY2NvdW50QGNvbnN0YW50LWN1YmlzdC0xNzMxMjMuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb21cIixcbiAgXCJjbGllbnRfaWRcIjogXCIxMDkyODcyODAxMzE5ODQ2MTA2MTZcIixcbiAgXCJhdXRoX3VyaVwiOiBcImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi9hdXRoXCIsXG4gIFwidG9rZW5fdXJpXCI6IFwiaHR0cHM6Ly9vYXV0aDIuZ29vZ2xlYXBpcy5jb20vdG9rZW5cIixcbiAgXCJhdXRoX3Byb3ZpZGVyX3g1MDlfY2VydF91cmxcIjogXCJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjEvY2VydHNcIixcbiAgXCJjbGllbnRfeDUwOV9jZXJ0X3VybFwiOiBcImh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL3JvYm90L3YxL21ldGFkYXRhL3g1MDkva2YtYWNjb3VudCU0MGNvbnN0YW50LWN1YmlzdC0xNzMxMjMuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb21cIlxufSIsImVtYWlsIjoia2YtYWNjb3VudEBjb25zdGFudC1jdWJpc3QtMTczMTIzLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwiYXV0aCI6IlgycHpiMjVmYTJWNU9uc0tJQ0FpZEhsd1pTSTZJQ0p6WlhKMmFXTmxYMkZqWTI5MWJuUWlMQW9nSUNKd2NtOXFaV04wWDJsa0lqb2dJbU52Ym5OMFlXNTBMV04xWW1semRDMHhOek14TWpNaUxBb2dJQ0p3Y21sMllYUmxYMnRsZVY5cFpDSTZJQ0poWkdJek5qY3pZMkk1T1RNM09USTJObUpqWXpGa05UVmlZakZsTjJKa01XVmpNemswWWpWaklpd0tJQ0FpY0hKcGRtRjBaVjlyWlhraU9pQWlMUzB0TFMxQ1JVZEpUaUJRVWtsV1FWUkZJRXRGV1MwdExTMHRYRzVOU1VsRmRsRkpRa0ZFUVU1Q1oydHhhR3RwUnpsM01FSkJVVVZHUVVGVFEwSkxZM2RuWjFOcVFXZEZRVUZ2U1VKQlVVTTJiek0zU2poTGFURlJhbmRGWEc1NFRVOTBUbEZXV2l0c2JGbE1TSFpUVjFkclEzbDZkV3R5Y0d4M1dVVlBTa1pPVlVkTk1rTnpja2g2WXpOS1ExQTRSbUZxTlVWVVJ6STViMDlhUzFaQ1hHNU1TalUzZVZkS1NFcHlla2hJYjJKeU9ITnNOeXRwY2pSallVb3ZTek5pUzJseWJtWldZVFpGZVhrNWF6RklhMFJNU2xaNFQxbHNhWEZUYmtkdFJsWjVYRzVEZVdsaGVXMDFNalZYZFdwcWRraENSRnBsUjJ4ak9XcHZWRXN3YjNKQmRpOVFLMkZuT0dWNVJTOWpUa05MUVhCT1NUaGxNVmRpYVdFd1UwSjFZVEJ1WEc1VldXeFFkVVYwY1hjeWR6UTRTVzVJZW1wRlVHTkZablJEYzFvd1RtUm9aR040VTNWVGJrbFFlVFZ2Ym10aWJsVjRXVnB3TTFJeGQwNWtOM2czV2tKT1hHNUVTbUZDV0VKVE1sVmtSMU0wZGl0emVXSlZRbFUxYVhGQmNrUk5iVk5tV1V3eE4wOVRVM1p6ZEVaU2RWRXlka0phYTBNM1RVOTZSV2QyTVVsSVpqQlhYRzV2T1U1TE1FVm9ka0ZuVFVKQlFVVkRaMmRGUVVOU2JVeHVSbHBMTWs1RWNXdDFUMlJHUW5kMmNqQmhOMmhqWTB0NWJtazVhSEZaUkZwSVJFMU5OMjVKWEc1bE9XbEpNVGRtYVRWb01qRm5UWGxUTlRsSE1ITnRTa3hsZEZBeWNGSnJRbnBoYlhZM1l6QnNNRFJxZGxReGFUTnlNV2RLUlZsTlRuTlZkRlIyUlVSdFhHNDJPVW9yV2tSRFRqYzNLMUZZUzIxRFEydFpLelJIVW1WaWVIaGpWMGRvTkM5TVVqWnJkMFk1UWk5b1YxSlRMMnhCZGxaTmMxWm1WalJ5SzNKVFpWTmpYRzV0VFVvNU5FRlJORTR6ZUhKWFJWRnpkV2wzVmtobVYweDBTVXhOWmtZM1VtaFhkWE4yTW1KbldDOUVLelJxTjBoSWRHZzROV3RpVnpGTE5IUXljV1EzWEc1Q01HaENSSEZaVUV4TFkyTXlWVFpEU1VkRFVXZFlkMHh6WldGRk1VWktiV0ZxYkhaelZTdEtWM1ptTmpKbVV6WlBNRXBWVEhsUlN6TTJaSE0yWkVaU1hHNXFhRGcxVFdKc1pWbEhNV2RwYUZwUFRYSnRjRU52V2tsVWF6ZEZUMDFsVTJwUVowVmFXRzVwVlZGTFFtZFJSRE50VUhKclptVktWV05YV21wTlpuZENYRzVpY2trMFRVMUZhWFpIVWtsVU5IbEhObkZrZG1sa1VpdDNWVGQyTXpGc2JVNXJVRGRMYXpncldHaEJhMFl5VFdsQ1JGSnFRVVpXYnlzMGREZHJlbHBFWEc1T09XWlBUVXBZTUdwR1oxRmFTMm94YmtkWU1YcEdkbVJFYWtVNGQyVmtVemRtVEZkd1RuTXlaV1p1UmxsS1VFWnVVa1ZOZW5odk5WVnFSalUyZUZkYVhHNVpRbUkyVkhsTmFUTlJhMWxFYmxBMFdUVmpUVVpDVlVwaVVVdENaMUZFUVN0UE5EbFVjMkV5WVdkV1VuSlliQzh4UkROeldIZDBVamRLU0dodVJVUmtYRzQxYVZrelFXMDVWREZYYWxWVk1UVlBhWEJOVEU1cGVrRnZlVkZZYVVwRldWb3lZMjVFZG1kc2QyUTBTeTh4WVV0dUszSnplSE5aUjBWbU9HaFpVRWh5WEc1Q2FEZHhibmx1T0VzeWJIa3lhR3BGTVdOTWFWUllPRFJGWjNkVlRIQlNZM2hxTjI1RE5HVmhTemhIU1hsSFN6WjJhMGR6YURRck5XNXlTMVJaWkdsTFhHNU1lVWhTTVVjMmNubDNTMEpuVVVSbkx6SnFTR0ZOYm1FeVN6UnNZVVV2VFdOWE5rMDVNbXRpUTNJelMzQkdaR05hZWtzclptazNWeTlSTW1oc05FdHFYRzVEY0RobFUwTldOREZJZFhkblNIYzJaR295ZHpGaFdFZzBRV0Y0ZUcxWmFVVlZXVkF2YTBSWE1sRTFVak14WkZjd2MyZHZOVWwwTmxKNVNtaFNkMlpUWEc1aFdqaHhhRTlqWTBONE16VjVhbWxsT1VsMVFUWXhZVEpVYTFoa2REZzJTamhVVFZGU1oyWXdOelEwVEVOV09VUnJhVk0xSzJsdVpuaFJTMEpuUlRGWFhHNU9iSGxhY1hGbVIyMDNWV1JQWm14U0wxUk5lVGhDTVRSSGQzSTFSRlZKYUVRMlYzbE5lREk1UWtwTk4ybHBjMlF2UlhCakwzUnBRbE5YUTNCSFkxWllYRzVCTkRoNWRqVTJZVTFNZG14cmVrSm9XVTE0WkRaV1ZHSmtOREZSUW1kUmVqUnpXVk16WTJWNlQydExUMUkyYW5wS2JsSTVja2xQZWt3cldWTjFUMFJ3WEc1cWNVbFpRemxPYzNZNVduRjNTelp2ZFhwa1F6WlJXSGxLVVRGUFFraE9NRFpwTDIwelEyVTNRVzlIUVZOTmNFVXJiSEE1Y1ZkbVZtRjVSbGRyWlZsUVhHNU9PRmhJZDJGTlVXTmtUMFprYkRaRmRsRjBaV3RRWTB4aVExRjZVelJTZEVoQlQwMU5URE41Y2k5RFFVazVTbVprYW5oV01IZGljVzFvTmxKM1dGQXpYRzRyT0dRNVVtazJPR3d3WlhjMVNFc3dNbFpFY1dGa1R5ODVNbUZvYzJzMlpqRlhWazR2UjB4d1dEaGlUMDFrUmtWMmNrNUxUM05SV1RSRmIwTlhRbE5yWEc1UmRXWmtRWFJXYm5oTlZHUnZjblV6Y1dORGVFUnRiejFjYmkwdExTMHRSVTVFSUZCU1NWWkJWRVVnUzBWWkxTMHRMUzFjYmlJc0NpQWdJbU5zYVdWdWRGOWxiV0ZwYkNJNklDSnJaaTFoWTJOdmRXNTBRR052Ym5OMFlXNTBMV04xWW1semRDMHhOek14TWpNdWFXRnRMbWR6WlhKMmFXTmxZV05qYjNWdWRDNWpiMjBpTEFvZ0lDSmpiR2xsYm5SZmFXUWlPaUFpTVRBNU1qZzNNamd3TVRNeE9UZzBOakV3TmpFMklpd0tJQ0FpWVhWMGFGOTFjbWtpT2lBaWFIUjBjSE02THk5aFkyTnZkVzUwY3k1bmIyOW5iR1V1WTI5dEwyOHZiMkYxZEdneUwyRjFkR2dpTEFvZ0lDSjBiMnRsYmw5MWNta2lPaUFpYUhSMGNITTZMeTl2WVhWMGFESXVaMjl2WjJ4bFlYQnBjeTVqYjIwdmRHOXJaVzRpTEFvZ0lDSmhkWFJvWDNCeWIzWnBaR1Z5WDNnMU1EbGZZMlZ5ZEY5MWNtd2lPaUFpYUhSMGNITTZMeTkzZDNjdVoyOXZaMnhsWVhCcGN5NWpiMjB2YjJGMWRHZ3lMM1l4TDJObGNuUnpJaXdLSUNBaVkyeHBaVzUwWDNnMU1EbGZZMlZ5ZEY5MWNtd2lPaUFpYUhSMGNITTZMeTkzZDNjdVoyOXZaMnhsWVhCcGN5NWpiMjB2Y205aWIzUXZkakV2YldWMFlXUmhkR0V2ZURVd09TOXJaaTFoWTJOdmRXNTBKVFF3WTI5dWMzUmhiblF0WTNWaWFYTjBMVEUzTXpFeU15NXBZVzB1WjNObGNuWnBZMlZoWTJOdmRXNTBMbU52YlNJS2ZRPT0ifX19
//kind: Secret
//metadata:
// name: docker-secret
//type: kubernetes.io/dockerconfigjson`,
// want: []string{"3aBcDFE5678901234567890_1a2b3c4d"},
// },
{
name: "DOCKER_AUTH_CONFIG escaped",
input: `[[runners]]
name = "docker-test@236"
url = "http://10.88.26.237:80"
executor = "docker"
environment = ["DOCKER_AUTH_CONFIG={\"auths\":{\"docker.contoso.com.tw:8083\":{\"auth\":\"c2Zjcy50ZXN0ZXI6c2Zjcw==\"}}}"]
[runners.custom_build_dir]
[runners.cache]
Insecure = false`,
want: []string{`{"registry":"docker.contoso.com.tw:8083","auth":"c2Zjcy50ZXN0ZXI6c2Zjcw=="}`},
},
{
name: "multiple escapes",
input: `[[runners]]
environment = ["DOCKER_AUTH_CONFIG={\\\"auths\\\":{\\\"docker.contoso.com.tw:8081\\\":{\\\"auth\\\":\\\"c2Zjcy50ZXN0ZXI6c2Zjcw==\\\"}}}"]`,
want: []string{`{"registry":"docker.contoso.com.tw:8081","auth":"c2Zjcy50ZXN0ZXI6c2Zjcw=="}`},
},
{
name: "DOCKER_AUTH_CONFIG",
input: `variables:
DOCKER_DRIVER: overlay2
DOCKER_AUTH_CONFIG: '{"auths": {"local-docker.artifactory.university.edu.au": {"auth": "YmFtYm9vOmpoMkh6UnNRU3pad3liaDc="}}}'
`,
want: []string{`{"registry":"local-docker.artifactory.university.edu.au","auth":"YmFtYm9vOmpoMkh6UnNRU3pad3liaDc="}`},
},
{
name: "empty email string",
input: `{
"auths": {
"quay.io": {
"auth": "dHJ1ZmZsZWhvZzpiZDQyNzQ2Yy1hNzc3LTQ4ZDktYjBhMi04N2I2YzEzMjdkMDA=",
"email": ""
}
}
}`,
want: []string{`{"registry":"quay.io","auth":"dHJ1ZmZsZWhvZzpiZDQyNzQ2Yy1hNzc3LTQ4ZDktYjBhMi04N2I2YzEzMjdkMDA="}`},
},
{
name: "docker.io registry",
input: `{"auths":{"docker.io":{"auth": "dHJ1ZmZsZWhvZzpiZDQyNzQ2Yy1hNzc3LTQ4ZDktYjBhMi04N2I2YzEzMjdkMDA="}}}`,
want: []string{`{"registry":"index.docker.io","auth":"dHJ1ZmZsZWhvZzpiZDQyNzQ2Yy1hNzc3LTQ4ZDktYjBhMi04N2I2YzEzMjdkMDA="}`},
},
{
name: "registry with slashes",
input: `{"auths":{"https://index.docker.io/v2/":{"auth": "dHJ1ZmZsZWhvZzpiZDQyNzQ2Yy1hNzc3LTQ4ZDktYjBhMi04N2I2YzEzMjdkMDA="}}}`,
want: []string{`{"registry":"https://index.docker.io/v2/","auth":"dHJ1ZmZsZWhvZzpiZDQyNzQ2Yy1hNzc3LTQ4ZDktYjBhMi04N2I2YzEzMjdkMDA="}`},
},
{
name: "literal newlines",
input: `{\n\"auths\": {\n\"registry.company.com\": {\n\"username\": \"conexp\",\n\"password\": \"FTA@CNCF0n@zure3\",\n\"email\": \"user@mycompany.com\",\n\"auth\": \"Y29uZXhwOkZUQUBDTkNGMG5AenVyZTM=\"\n}\n}\n}\n`,
want: []string{`{"registry":"registry.company.com","auth":"Y29uZXhwOkZUQUBDTkNGMG5AenVyZTM="}`},
},
{
name: "literal newlines and tabs",
input: ` config.json: "{\n\t\"auths\": {\n\t\t\"https://index.docker.io/v2/\": {\n\t\t\t\"auth\":\"Y29uZXhwOkZUQUBDTkNGMG5AenVyZTM=\"\n\t\t}\n\t}\n}"`,
want: []string{`{"registry":"https://index.docker.io/v2/","auth":"Y29uZXhwOkZUQUBDTkNGMG5AenVyZTM="}`},
},
{
name: "content after last }",
// This is base64-encoded, however, that doesn't get detected in these tests.
//input: `{"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1","request":{"uid":"b9d17c49-1b2c-421a-8ae8-3b3d252d2f61","kind":{"group":"","version":"v1","kind":"Secret"},"resource":{"group":"","version":"v1","resource":"secrets"},"requestKind":{"group":"","version":"v1","kind":"Secret"},"requestResource":{"group":"","version":"v1","resource":"secrets"},"name":"regcred","namespace":"test-webhooks","operation":"CREATE","userInfo":{"username":"kube:admin","groups":["system:cluster-admins","system:authenticated"],"extra":{"scopes.authorization.openshift.io":["user:full"]}},"object":{"kind":"Secret","apiVersion":"v1","metadata":{"name":"regcred","namespace":"test-webhooks","uid":"544674ac-f0fb-4a30-994b-eab579e1f418","creationTimestamp":"2022-05-03T15:16:55Z","managedFields":[{"manager":"kubectl-create","operation":"Update","apiVersion":"v1","time":"2022-05-03T15:16:55Z","fieldsType":"FieldsV1","fieldsV1":{"f:data":{".":{},"f:.dockerconfigjson":{}},"f:type":{}}}]},"data":{".dockerconfigjson":"eyJhdXRocyI6eyJxdWF5LmlvIjp7InVzZXJuYW1lIjoiMTIzIiwicGFzc3dvcmQiOiIxMjMiLCJhdXRoIjoiTVRJek9qRXlNdz09In19fQ=="},"type":"kubernetes.io/dockerconfigjson"},"oldObject":null,"dryRun":false,"options":{"kind":"CreateOptions","apiVersion":"meta.k8s.io/v1","fieldManager":"kubectl-create"}}}`,
input: `{"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1","request":{"uid":"b9d17c49-1b2c-421a-8ae8-3b3d252d2f61","kind":{"group":"","version":"v1","kind":"Secret"},"resource":{"group":"","version":"v1","resource":"secrets"},"requestKind":{"group":"","version":"v1","kind":"Secret"},"requestResource":{"group":"","version":"v1","resource":"secrets"},"name":"regcred","namespace":"test-webhooks","operation":"CREATE","userInfo":{"username":"kube:admin","groups":["system:cluster-admins","system:authenticated"],"extra":{"scopes.authorization.openshift.io":["user:full"]}},"object":{"kind":"Secret","apiVersion":"v1","metadata":{"name":"regcred","namespace":"test-webhooks","uid":"544674ac-f0fb-4a30-994b-eab579e1f418","creationTimestamp":"2022-05-03T15:16:55Z","managedFields":[{"manager":"kubectl-create","operation":"Update","apiVersion":"v1","time":"2022-05-03T15:16:55Z","fieldsType":"FieldsV1","fieldsV1":{"f:data":{".":{},"f:.dockerconfigjson":{}},"f:type":{}}}]},"data":{".dockerconfigjson":"{"auths":{"quay.io":{"username":"123","password":"123","auth":"MTIzOjEyMw=="}}}"},"type":"kubernetes.io/dockerconfigjson"},"oldObject":null,"dryRun":false,"options":{"kind":"CreateOptions","apiVersion":"meta.k8s.io/v1","fieldManager":"kubectl-create"}}}`,
want: []string{`{"registry":"quay.io","auth":"MTIzOjEyMw=="}`},
},
// False-positives
{
name: "registry.example.com",
input: `1. Modify the runner's config.toml file as follows:
[[runners]]
environment = ["DOCKER_AUTH_CONFIG={\"auths\":{\"registry.example.com:5000\":{\"auth\":\"bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ=\"}}}"]
`,
},
{
name: "",
input: `sudo gitlab-runner register -n \
--url https://gitlab.contoso.cn:8443/ \
--registration-token ****** \
--docker-extra-hosts "gitlab.contoso.cn:10.202.101.22" \
--tag-list "golang-test" \
--executor docker \
--description "229 contoso golang test" \
--docker-image "docker:19.03.1" \
--docker-privileged \
--env "DOCKER_AUTH_CONFIG={\"auths\": {\"registry.contoso123.cn:5000\": {\"auth\": \"******\"},\"registry.contoso.com.cn\": {\"auth\": \"******\"}}}" \
--custom_build_dir-enabled=true `,
},
// TODO: There's currently no solution to detect/ignore environment variables or placeholders.
// {
// name: "variables",
// input: `analyze_reports:
//stage: post
//image: registry.gitlab.com/detecttechnologies/software/webapps/t-pulse/web/tpulse-msa/tpulse-msa-cicd:production
//variables:
// DOCKER_AUTH_CONFIG: '{"auths":{"registry.gitlab.com":{"username":"${CI_CD_API_USER}","password":"${CI_CD_API_TOKEN}"}}}'`,
// },
{
name: "empty registry",
input: `The command outputs the following:
* A non-bootable configuration ISO ( agentconfig.noarch.iso)
* 'auth' directory: contains kubeconfig and kubeadmin-password
Note: for disconnected environments, specify a dummy pull-secret in install-config.yaml (e.g. '{"auths":{"":{"auth":"dXNlcjpwYXNz"}}}').`,
},
}
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, 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)
}
})
}
}
func Test_ParseAuth(t *testing.T) {
tests := map[dockerAuth]string{
// Only auth
dockerAuth{
Auth: "Ym9iOnMzY3IzdHBAc3N3MHJkIQ==",
}: "bob:s3cr3tp@ssw0rd!",
// Auth with colon
dockerAuth{
Auth: "OTM5MDQ5YjQtNTllMS00YzlhLWJlYzgtMjAyZTAxZjc2MWFlOjZCLkpFOmZPT2hvLTI3P244TlYybDZqQS9UdjBMd1hm",
}: "939049b4-59e1-4c9a-bec8-202e01f761ae:6B.JE:fOOho-27?n8NV2l6jA/Tv0LwXf",
// Only username + password
dockerAuth{
Username: "my_username",
Password: "my_password",
}: "my_username:my_password",
// Auth and username+password
dockerAuth{
Auth: "bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ==",
Username: "my_username",
Password: "my_password",
}: "my_username:my_password",
// Kubernetes public test credentials
// https://github.com/kubernetes/autoscaler/blob/f22b40eab867cbc52bdb15dc8768962e21d22837/vertical-pod-autoscaler/e2e/vendor/k8s.io/kubernetes/test/e2e/common/node/runtime.go#L283C1-L290C2
dockerAuth{
Auth: `X2pzb25fa2V5OnsKICAidHlwZSI6ICJzZXJ2aWNlX2FjY291bnQiLAogICJwcm9qZWN0X2lkIjogImF1dGhlbnRpY2F0ZWQtaW1hZ2UtcHVsbGluZyIsCiAgInByaXZhdGVfa2V5X2lkIjogImI5ZjJhNjY0YWE5YjIwNDg0Y2MxNTg2MDYzZmVmZGExOTIyNGFjM2IiLAogICJwcml2YXR
lX2tleSI6ICItLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS1cbk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQzdTSG5LVEVFaVlMamZcbkpmQVBHbUozd3JCY2VJNTBKS0xxS21GWE5RL3REWGJRK2g5YVl4aldJTDhEeDBKZTc0bVovS01uV2dYRjVLWlNcbm9BNktuSU85Yi9SY1NlV2V
pSXRSekkzL1lYVitPNkNjcmpKSXl4anFWam5mVzJpM3NhMzd0OUE5VEZkbGZycm5cbjR6UkpiOWl4eU1YNGJMdHFGR3ZCMDNOSWl0QTNzVlo1ODhrb1FBZmgzSmhhQmVnTWorWjRSYko0aGVpQlFUMDNcbnZVbzViRWFQZVQ5RE16bHdzZWFQV2dydDZOME9VRGNBRTl4bGNJek11MjUzUG4vSzgySFpydEx4akd2UkhNVXhcbng0Zjh
wSnhmQ3h4QlN3Z1NORit3OWpkbXR2b0wwRmE3ZGducFJlODZWRDY2ejNZenJqNHlLRXRqc2hLZHl5VWRcbkl5cVhoN1JSQWdNQkFBRUNnZ0VBT3pzZHdaeENVVlFUeEFka2wvSTVTRFVidi9NazRwaWZxYjJEa2FnbmhFcG9cbjFJajJsNGlWMTByOS9uenJnY2p5VlBBd3pZWk1JeDFBZVF0RDdoUzRHWmFweXZKWUc3NkZpWFpQUm9
DVlB6b3VcbmZyOGRDaWFwbDV0enJDOWx2QXNHd29DTTdJWVRjZmNWdDdjRTEyRDNRS3NGNlo3QjJ6ZmdLS251WVBmK0NFNlRcbmNNMHkwaCtYRS9kMERvSERoVy96YU1yWEhqOFRvd2V1eXRrYmJzNGYvOUZqOVBuU2dET1lQd2xhbFZUcitGUWFcbkpSd1ZqVmxYcEZBUW14M0Jyd25rWnQzQ2lXV2lGM2QrSGk5RXRVYnRWclcxYjZ
nK1JRT0licWFtcis4YlJuZFhcbjZWZ3FCQWtKWjhSVnlkeFVQMGQxMUdqdU9QRHhCbkhCbmM0UW9rSXJFUUtCZ1FEMUNlaWN1ZGhXdGc0K2dTeGJcbnplanh0VjFONDFtZHVjQnpvMmp5b1dHbzNQVDh3ckJPL3lRRTM0cU9WSi9pZCs4SThoWjRvSWh1K0pBMDBzNmdcblRuSXErdi9kL1RFalk4MW5rWmlDa21SUFdiWHhhWXR4UjI
xS1BYckxOTlFKS2ttOHRkeVh5UHFsOE1veUdmQ1dcbjJ2aVBKS05iNkhabnY5Q3lqZEo5ZzJMRG5RS0JnUUREcVN2eURtaGViOTIzSW96NGxlZ01SK205Z2xYVWdTS2dcbkVzZlllbVJmbU5XQitDN3ZhSXlVUm1ZNU55TXhmQlZXc3dXRldLYXhjK0krYnFzZmx6elZZdFpwMThNR2pzTURcbmZlZWZBWDZCWk1zVXQ3Qmw3WjlWSjg
1bnRFZHFBQ0xwWitaLzN0SVJWdWdDV1pRMWhrbmxHa0dUMDI0SkVFKytcbk55SDFnM2QzUlFLQmdRQ1J2MXdKWkkwbVBsRklva0tGTkh1YTBUcDNLb1JTU1hzTURTVk9NK2xIckcxWHJtRjZcbkMwNGNTKzQ0N0dMUkxHOFVUaEpKbTRxckh0Ti9aK2dZOTYvMm1xYjRIakpORDM3TVhKQnZFYTN5ZUxTOHEvK1JcbjJGOU1LamRRaU5
LWnhQcG84VzhOSlREWTVOa1BaZGh4a2pzSHdVNGRTNjZwMVRESUU0MGd0TFpaRFFLQmdGaldcbktyblFpTnEzOS9iNm5QOFJNVGJDUUFKbmR3anhTUU5kQTVmcW1rQTlhRk9HbCtqamsxQ1BWa0tNSWxLSmdEYkpcbk9heDl2OUc2Ui9NSTFIR1hmV3QxWU56VnRocjRIdHNyQTB0U3BsbWhwZ05XRTZWejZuQURqdGZQSnMyZUdqdlh
cbmpQUnArdjhjY21MK3dTZzhQTGprM3ZsN2VlNXJsWWxNQndNdUdjUHhBb0dBZWRueGJXMVJMbVZubEFpSEx1L0xcbmxtZkF3RFdtRWlJMFVnK1BMbm9Pdk81dFE1ZDRXMS94RU44bFA0cWtzcGtmZk1Rbk5oNFNZR0VlQlQzMlpxQ1RcbkpSZ2YwWGpveXZ2dXA5eFhqTWtYcnBZL3ljMXpmcVRaQzBNTzkvMVVjMWJSR2RaMmR5M2x
SNU5XYXA3T1h5Zk9cblBQcE5Gb1BUWGd2M3FDcW5sTEhyR3pNPVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwKICAiY2xpZW50X2VtYWlsIjogImltYWdlLXB1bGxpbmdAYXV0aGVudGljYXRlZC1pbWFnZS1wdWxsaW5nLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwKICAiY2xpZW50X2lkIjogIjExMzc5NzkxNDUzMDA
3MzI3ODcxMiIsCiAgImF1dGhfdXJpIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi9hdXRoIiwKICAidG9rZW5fdXJpIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsCiAgImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjEvY2VydHMiLAogICJjbGllbnRfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9yb2JvdC92MS9tZXRhZGF0YS94NTA5L2ltYWdlLXB1bGxpbmclNDBhdXRoZW50aWNhdGVkLWltYWdlLXB1bGxpbmcuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iCn0=`,
}: "_json_key:{\n \"type\": \"service_account\",\n \"project_id\": \"authenticated-image-pulling\",\n \"private_key_id\": \"b9f2a664aa9b20484cc1586063fefda19224ac3b\",\n \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7SHnKTEEiYLjf\\nJfAPGmJ3wrBceI50JKLqKmFXNQ/tDXbQ+h9aYxjWIL8Dx0Je74mZ/KMnWgXF5KZS\\noA6KnIO9b/RcSeWeiItRzI3/YXV+O6CcrjJIyxjqVjnfW2i3sa37t9A9TFdlfrrn\\n4zRJb9ixyMX4bLtqFGvB03NIitA3sVZ588koQAfh3JhaBegMj+Z4RbJ4heiBQT03\\nvUo5bEaPeT9DMzlwseaPWgrt6N0OUDcAE9xlcIzMu253Pn/K82HZrtLxjGvRHMUx\\nx4f8pJxfCxxBSwgSNF+w9jdmtvoL0Fa7dgnpRe86VD66z3Yzrj4yKEtjshKdyyUd\\nIyqXh7RRAgMBAAECggEAOzsdwZxCUVQTxAdkl/I5SDUbv/Mk4pifqb2DkagnhEpo\\n1Ij2l4iV10r9/nzrgcjyVPAwzYZMIx1AeQtD7hS4GZapyvJYG76FiXZPRoCVPzou\\nfr8dCiapl5tzrC9lvAsGwoCM7IYTcfcVt7cE12D3QKsF6Z7B2zfgKKnuYPf+CE6T\\ncM0y0h+XE/d0DoHDhW/zaMrXHj8Toweuytkbbs4f/9Fj9PnSgDOYPwlalVTr+FQa\\nJRwVjVlXpFAQmx3BrwnkZt3CiWWiF3d+Hi9EtUbtVrW1b6g+RQOIbqamr+8bRndX\\n6VgqBAkJZ8RVydxUP0d11GjuOPDxBnHBnc4QokIrEQKBgQD1CeicudhWtg4+gSxb\\nzejxtV1N41mducBzo2jyoWGo3PT8wrBO/yQE34qOVJ/id+8I8hZ4oIhu+JA00s6g\\nTnIq+v/d/TEjY81nkZiCkmRPWbXxaYtxR21KPXrLNNQJKkm8tdyXyPql8MoyGfCW\\n2viPJKNb6HZnv9CyjdJ9g2LDnQKBgQDDqSvyDmheb923Ioz4legMR+m9glXUgSKg\\nEsfYemRfmNWB+C7vaIyURmY5NyMxfBVWswWFWKaxc+I+bqsflzzVYtZp18MGjsMD\\nfeefAX6BZMsUt7Bl7Z9VJ85ntEdqACLpZ+Z/3tIRVugCWZQ1hknlGkGT024JEE++\\nNyH1g3d3RQKBgQCRv1wJZI0mPlFIokKFNHua0Tp3KoRSSXsMDSVOM+lHrG1XrmF6\\nC04cS+447GLRLG8UThJJm4qrHtN/Z+gY96/2mqb4HjJND37MXJBvEa3yeLS8q/+R\\n2F9MKjdQiNKZxPpo8W8NJTDY5NkPZdhxkjsHwU4dS66p1TDIE40gtLZZDQKBgFjW\\nKrnQiNq39/b6nP8RMTbCQAJndwjxSQNdA5fqmkA9aFOGl+jjk1CPVkKMIlKJgDbJ\\nOax9v9G6R/MI1HGXfWt1YNzVthr4HtsrA0tSplmhpgNWE6Vz6nADjtfPJs2eGjvX\\njPRp+v8ccmL+wSg8PLjk3vl7ee5rlYlMBwMuGcPxAoGAednxbW1RLmVnlAiHLu/L\\nlmfAwDWmEiI0Ug+PLnoOvO5tQ5d4W1/xEN8lP4qkspkffMQnNh4SYGEeBT32ZqCT\\nJRgf0Xjoyvvup9xXjMkXrpY/yc1zfqTZC0MO9/1Uc1bRGdZ2dy3lR5NWap7OXyfO\\nPPpNFoPTXgv3qCqnlLHrGzM=\\n-----END PRIVATE KEY-----\\n\",\n \"client_email\": \"image-pulling@authenticated-image-pulling.iam.gserviceaccount.com\",\n \"client_id\": \"113797914530073278712\",\n \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n \"token_uri\": \"https://accounts.google.com/o/oauth2/token\",\n \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\n \"client_x509_cert_url\": \"https://www.googleapis.com/robot/v1/metadata/x509/image-pulling%40authenticated-image-pulling.iam.gserviceaccount.com\"\n}",
// Errors
// Auth isn't `username:password` format.
dockerAuth{
Auth: "dGhpc2lzYXN0cmluZ3dpdGhvdXRhbnljb2xvbg==",
}: "",
// Invalid base64
dockerAuth{
Auth: "asda42asd214ASDKqwwq==",
}: "",
}
ctx := context.Background()
for input, expected := range tests {
username, password, encoded := parseBasicAuth(ctx.Logger(), input)
if expected == "" {
if encoded != "" {
t.Errorf("expected an error, got: username=%s, password=%s, encoded=%s", username, password, encoded)
}
continue
}
if diff := cmp.Diff(expected, username+":"+password); diff != "" {
t.Errorf("%s diff: (-want +got)\n%s", input, diff)
}
}
}
func Test_ParseAuthenticateHeader(t *testing.T) {
tests := map[string]map[string]string{
`Bearer realm="https://auth.docker.io/token",service="registry.docker.io"`: {
"scheme": "Bearer",
"realm": "https://auth.docker.io/token",
"service": "registry.docker.io",
},
`Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull"`: {
"scheme": "Bearer",
"realm": "https://ghcr.io/token",
"service": "ghcr.io",
"scope": "repository:user/image:pull",
},
`Bearer realm="https://artifactory.example.com:443/artifactory/api/docker/docker-repo/v2/token",service="artifactory.example.com:443"`: {
"scheme": "Bearer",
"realm": "https://artifactory.example.com:443/artifactory/api/docker/docker-repo/v2/token",
"service": "artifactory.example.com:443",
},
}
for input, expected := range tests {
actual, err := parseAuthenticateHeader(input)
if err != nil {
t.Errorf("failed to parse www-authenticate header: %v", err)
}
if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf("%s diff: (-want +got)\n%s", input, diff)
}
}
}
================================================
FILE: pkg/detectors/dockerhub/v1/dockerhub.go
================================================
package dockerhub
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v5"
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/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
func (s Scanner) Version() int { return 1 }
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)
var (
// Can use email or username for login.
usernamePat = regexp.MustCompile(detectors.PrefixRegex([]string{"docker"}) + `(?im)(?:user|usr|-u|id)\S{0,40}?[:=\s]{1,3}[ '"=]?([a-zA-Z0-9]{4,40})\b`)
emailPat = regexp.MustCompile(detectors.PrefixRegex([]string{"docker"}) + common.EmailPattern)
// Can use password or personal access token (PAT) for login, but this scanner will only check for PATs.
accessTokenPat = regexp.MustCompile(detectors.PrefixRegex([]string{"docker"}) + `\b([a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})\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{"docker"}
}
// FromData will find and optionally verify Dockerhub 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)
// Deduplicate results.
tokens := make(map[string]struct{})
for _, matches := range accessTokenPat.FindAllStringSubmatch(dataStr, -1) {
tokens[matches[1]] = struct{}{}
}
if len(tokens) == 0 {
return
}
usernames := make(map[string]struct{})
for _, matches := range usernamePat.FindAllStringSubmatch(dataStr, -1) {
usernames[matches[1]] = struct{}{}
}
for _, matches := range emailPat.FindAllStringSubmatch(dataStr, -1) {
usernames[matches[1]] = struct{}{}
}
// Process results.
for token := range tokens {
s1 := detectors.Result{
DetectorType: s.Type(),
Raw: []byte(token),
}
for username := range usernames {
s1.RawV2 = []byte(fmt.Sprintf("%s:%s", username, token))
if verify {
if s.client == nil {
s.client = common.SaneHttpClient()
}
isVerified, extraData, verificationErr := s.verifyMatch(ctx, username, token)
s1.Verified = isVerified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr)
if s1.Verified {
s1.AnalysisInfo = map[string]string{
"username": username,
"pat": token,
}
}
}
results = append(results, s1)
if s1.Verified {
break
}
}
// PAT matches without usernames cannot be verified but might still be useful.
if len(usernames) == 0 {
results = append(results, s1)
}
}
return
}
func (s Scanner) verifyMatch(ctx context.Context, username string, password string) (bool, map[string]string, error) {
payload := strings.NewReader(fmt.Sprintf(`{"username": "%s", "password": "%s"}`, username, password))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://hub.docker.com/v2/users/login", payload)
if err != nil {
return false, nil, err
}
req.Header.Add("Content-Type", "application/json")
res, err := s.client.Do(req)
if err != nil {
return false, nil, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return false, nil, err
}
if res.StatusCode == http.StatusOK {
var tokenRes tokenResponse
if err := json.Unmarshal(body, &tokenRes); (err != nil || tokenRes == tokenResponse{}) {
return false, nil, err
}
parser := jwt.NewParser()
token, _, err := parser.ParseUnverified(tokenRes.Token, &hubJwtClaims{})
if err != nil {
return true, nil, err
}
if claims, ok := token.Claims.(*hubJwtClaims); ok {
extraData := map[string]string{
"hub_username": username,
"hub_email": claims.HubClaims.Email,
"hub_scope": claims.Scope,
}
return true, extraData, nil
}
return true, nil, nil
} else if res.StatusCode == http.StatusUnauthorized {
// Valid credentials can still return a 401 status code if 2FA is enabled
var mfaRes mfaRequiredResponse
if err := json.Unmarshal(body, &mfaRes); err != nil || mfaRes.MfaToken == "" {
return false, nil, nil
}
extraData := map[string]string{
"hub_username": username,
"2fa_required": "true",
}
return true, extraData, nil
} else {
return false, nil, fmt.Errorf("unexpected response status %d", res.StatusCode)
}
}
type tokenResponse struct {
Token string `json:"token"`
}
type userClaims struct {
Username string `json:"username"`
Email string `json:"email"`
}
type hubJwtClaims struct {
Scope string `json:"scope"`
HubClaims userClaims `json:"https://hub.docker.com"` // not sure why this is a key, further investigation required.
jwt.RegisteredClaims
}
type mfaRequiredResponse struct {
MfaToken string `json:"login_2fa_token"`
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Dockerhub
}
func (s Scanner) Description() string {
return "Docker is a platform used to develop, ship, and run applications. Docker access tokens can be used to authenticate and interact with Docker services."
}
================================================
FILE: pkg/detectors/dockerhub/v1/dockerhub_integration_test.go
================================================
//go:build detectors
// +build detectors
package dockerhub
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDockerhub_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
username := testSecrets.MustGetField("DOCKERHUB_USERNAME")
email := testSecrets.MustGetField("DOCKERHUB_EMAIL")
pat := testSecrets.MustGetField("DOCKERHUB_PAT")
inactivePat := testSecrets.MustGetField("DOCKERHUB_INACTIVE_PAT")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("docker login -u %s -p %s", username, pat)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dockerhub,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, verified (email)",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("docker login -u %s -p %s", email, pat)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dockerhub,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("docker login -u %s -p %s", username, inactivePat)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dockerhub,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Dockerhub.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])
}
got[i].Raw = nil
got[i].RawV2 = nil
got[i].ExtraData = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Dockerhub.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/dockerhub/v1/dockerhub_test.go
================================================
package dockerhub
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
in: ""
api_version: v1
secret: ""
base_url: "https://api.example.com/$api_version/examples"
response_code: 200
docker:
user: rRwOdIJpY90QrIzOXO95d3hlSzRk5Z9a
docker_email: "docker-test@dockerhub.com"
docker_token: "9jyxkwvk-rjnp-7eo1-1gtc-ruj6rqmiyapo"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secrets = []string{
"rRwOdIJpY90QrIzOXO95d3hlSzRk5Z9a:9jyxkwvk-rjnp-7eo1-1gtc-ruj6rqmiyapo",
"docker-test@dockerhub.com:9jyxkwvk-rjnp-7eo1-1gtc-ruj6rqmiyapo",
}
)
func TestDockerHub_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: secrets,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/dockerhub/v2/dockerhub.go
================================================
package dockerhub
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v5"
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/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
func (s Scanner) Version() int { return 2 }
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)
var (
// Can use email or username for login.
usernamePat = regexp.MustCompile(`(?im)(?:user|usr|-u|id)\S{0,40}?[:=\s]{1,3}[ '"=]?([a-zA-Z0-9]{4,40})\b`)
emailPat = regexp.MustCompile(common.EmailPattern)
// Can use password or personal/organization access token (PAT/OAT) for login, but this scanner will only check for PATs and OATs.
accessTokenPat = regexp.MustCompile(`\b(dckr_pat_[a-zA-Z0-9_-]{27}|dckr_oat_[a-zA-Z0-9_-]{32})(?:[^a-zA-Z0-9_-]|\z)`)
)
// 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{"docker", "dckr_pat_", "dckr_oat_"}
}
// FromData will find and optionally verify Dockerhub 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)
// Deduplicate results.
tokens := make(map[string]struct{})
for _, matches := range accessTokenPat.FindAllStringSubmatch(dataStr, -1) {
tokens[matches[1]] = struct{}{}
}
if len(tokens) == 0 {
return
}
usernames := make(map[string]struct{})
for _, matches := range usernamePat.FindAllStringSubmatch(dataStr, -1) {
usernames[matches[1]] = struct{}{}
}
for _, matches := range emailPat.FindAllStringSubmatch(dataStr, -1) {
usernames[matches[1]] = struct{}{}
}
// Process results.
for token := range tokens {
s1 := detectors.Result{
DetectorType: s.Type(),
Raw: []byte(token),
}
for username := range usernames {
s1.RawV2 = []byte(fmt.Sprintf("%s:%s", username, token))
if verify {
if s.client == nil {
s.client = common.SaneHttpClient()
}
isVerified, extraData, verificationErr := s.verifyMatch(ctx, username, token)
s1.Verified = isVerified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr)
if s1.Verified {
s1.AnalysisInfo = map[string]string{
"username": username,
"pat": token,
}
}
}
results = append(results, s1)
if s1.Verified {
break
}
}
// PAT matches without usernames cannot be verified but might still be useful.
if len(usernames) == 0 {
results = append(results, s1)
}
}
return
}
func (s Scanner) verifyMatch(ctx context.Context, username string, password string) (bool, map[string]string, error) {
payload := strings.NewReader(fmt.Sprintf(`{"identifier": "%s", "secret": "%s"}`, username, password))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://hub.docker.com/v2/auth/token", payload)
if err != nil {
return false, nil, err
}
req.Header.Add("Content-Type", "application/json")
res, err := s.client.Do(req)
if err != nil {
return false, nil, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return false, nil, err
}
if res.StatusCode == http.StatusOK {
var tokenRes tokenResponse
if err := json.Unmarshal(body, &tokenRes); (err != nil || tokenRes == tokenResponse{}) {
return false, nil, err
}
parser := jwt.NewParser()
token, _, err := parser.ParseUnverified(tokenRes.Token, &hubJwtClaims{})
if err != nil {
return true, nil, err
}
if claims, ok := token.Claims.(*hubJwtClaims); ok {
extraData := map[string]string{
"hub_username": username,
"hub_email": claims.HubClaims.Email,
"hub_scope": claims.Scope,
}
return true, extraData, nil
}
return true, nil, nil
} else if res.StatusCode == http.StatusUnauthorized {
// Valid credentials can still return a 401 status code if 2FA is enabled
var mfaRes mfaRequiredResponse
if err := json.Unmarshal(body, &mfaRes); err != nil || mfaRes.MfaToken == "" {
return false, nil, nil
}
extraData := map[string]string{
"hub_username": username,
"2fa_required": "true",
}
return true, extraData, nil
} else {
return false, nil, fmt.Errorf("unexpected response status %d", res.StatusCode)
}
}
type tokenResponse struct {
Token string `json:"access_token"`
}
type userClaims struct {
Username string `json:"username"`
Email string `json:"email"`
}
type hubJwtClaims struct {
Scope string `json:"scope"`
HubClaims userClaims `json:"https://hub.docker.com"` // not sure why this is a key, further investigation required.
jwt.RegisteredClaims
}
type mfaRequiredResponse struct {
MfaToken string `json:"login_2fa_token"`
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Dockerhub
}
func (s Scanner) Description() string {
return "Dockerhub is a cloud-based repository in which Docker users and partners create, test, store and distribute container images. Dockerhub personal access tokens (PATs) can be used to access and manage these container images."
}
================================================
FILE: pkg/detectors/dockerhub/v2/dockerhub_integration_test.go
================================================
//go:build detectors
// +build detectors
package dockerhub
import (
"context"
"fmt"
"strings"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDockerhub_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
username := testSecrets.MustGetField("DOCKERHUB_USERNAME")
email := testSecrets.MustGetField("DOCKERHUB_EMAIL")
pat := testSecrets.MustGetField("DOCKERHUB_PAT")
inactivePat := testSecrets.MustGetField("DOCKERHUB_INACTIVE_PAT")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("docker login -u %s -p %s", username, pat)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dockerhub,
Verified: true,
AnalysisInfo: map[string]string{
"username": username,
"pat": pat,
},
},
},
wantErr: false,
},
{
name: "found, verified (email)",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("docker login -u %s -p %s", email, pat)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dockerhub,
Verified: true,
AnalysisInfo: map[string]string{
"username": strings.Split(email, "-")[0],
"pat": pat,
},
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("docker login -u %s -p %s", username, inactivePat)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dockerhub,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Dockerhub.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])
}
got[i].Raw = nil
got[i].RawV2 = nil
got[i].ExtraData = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Dockerhub.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/dockerhub/v2/dockerhub_test.go
================================================
package dockerhub
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: ""
in: ""
api_version: v1
secret: ""
base_url: "https://api.example.com/$api_version/examples"
response_code: 200
docker:
user: rRwOdIJpY90QrIzOXO95d3hlSzRk5Z9a
docker_email: "docker-test@dockerhub.com"
docker_token: "dckr_pat_dlndn9l2JLhWvbdyP3blEZw_j7d"
docker_org_token: "dckr_oat_7bA9zRt5-JqX3vP0l_MnY8sK2wE-dF6h"
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secrets = []string{
"rRwOdIJpY90QrIzOXO95d3hlSzRk5Z9a:dckr_pat_dlndn9l2JLhWvbdyP3blEZw_j7d",
"docker-test@dockerhub.com:dckr_pat_dlndn9l2JLhWvbdyP3blEZw_j7d",
"rRwOdIJpY90QrIzOXO95d3hlSzRk5Z9a:dckr_oat_7bA9zRt5-JqX3vP0l_MnY8sK2wE-dF6h",
"docker-test@dockerhub.com:dckr_oat_7bA9zRt5-JqX3vP0l_MnY8sK2wE-dF6h",
}
)
func TestDockerHub_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: secrets,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/docparser/docparser.go
================================================
package docparser
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"docparser"}) + `\b([a-f0-9]{40})\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{"docparser"}
}
// FromData will find and optionally verify Docparser 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Docparser,
Raw: []byte(resMatch),
}
if verify {
url := fmt.Sprintf("https://api.docparser.com/v1/parsers?api_key=%s", resMatch)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Docparser
}
func (s Scanner) Description() string {
return "Docparser is a document processing service that extracts data from PDFs and scanned documents. Docparser API keys can be used to access and manipulate this data."
}
================================================
FILE: pkg/detectors/docparser/docparser_integration_test.go
================================================
//go:build detectors
// +build detectors
package docparser
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDocparser_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DOCPARSER")
inactiveSecret := testSecrets.MustGetField("DOCPARSER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a docparser secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Docparser,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a docparser secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Docparser,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Docparser.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Docparser.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/docparser/docparser_test.go
================================================
package docparser
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
api_version: v1
docparser_secret: "1761d026b1202108b5f9ecd28d1ecae826b0aee8"
base_url: "https://api.example.com/$api_version/example/api_key=$docparser_secret"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "1761d026b1202108b5f9ecd28d1ecae826b0aee8"
)
func TestDocParser_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/documo/documo.go
================================================
package documo
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(ey[a-zA-Z0-9]{34}.ey[a-zA-Z0-9]{154}.[a-zA-Z0-9_-]{43})\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{"documo"}
}
// FromData will find and optionally verify Documo 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Documo,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.documo.com/v1/me", nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Documo
}
func (s Scanner) Description() string {
return "A service for creating and modifying documents. API keys can create read update and delete documents."
}
================================================
FILE: pkg/detectors/documo/documo_integration_test.go
================================================
//go:build detectors
// +build detectors
package documo
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDocumo_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DOCUMO")
inactiveSecret := testSecrets.MustGetField("DOCUMO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a documo secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Documo,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a documo secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Documo,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Documo.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Documo.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/documo/documo_test.go
================================================
package documo
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Documo Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Basic"
in: "Path"
api_version: v1
secret: "eyS9YqgD6TgdQ943G8S3aaiz26m2fTN9rcPbpeyts0jBEFd43hEFfr9pC7voqvLsbEi7Px4TbMToCVrstQRe8r2kltKGWyChYCT1Iruo6p3g3PyqZaZ1gOSbjeXz8zARUHZkXo7XR86kape65HLXj59yCNIlW5bvebJYbIAjjgGAAmXVgzldvNv8Zs08KIS5y62QJSNcnipFQbnxA8z6TUMl0F600MJhqEILWo19GaGjw"
base_url: "https://api.example.com/$api_version/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "eyS9YqgD6TgdQ943G8S3aaiz26m2fTN9rcPbpeyts0jBEFd43hEFfr9pC7voqvLsbEi7Px4TbMToCVrstQRe8r2kltKGWyChYCT1Iruo6p3g3PyqZaZ1gOSbjeXz8zARUHZkXo7XR86kape65HLXj59yCNIlW5bvebJYbIAjjgGAAmXVgzldvNv8Zs08KIS5y62QJSNcnipFQbnxA8z6TUMl0F600MJhqEILWo19GaGjw"
)
func TestDocumo_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/docusign/docusign.go
================================================
package docusign
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"strings"
"github.com/go-errors/errors"
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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
type Response struct {
AccessToken string `json:"access_token"`
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"integration", "id"}) + common.UUIDPattern)
secretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"secret"}) + common.UUIDPattern)
)
// 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{"docusign"}
}
// FromData will find and optionally verify Docusign 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)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
secretMatches := secretPat.FindAllStringSubmatch(dataStr, -1)
for _, idMatch := range idMatches {
resIDMatch := strings.TrimSpace(idMatch[1])
for _, secretMatch := range secretMatches {
resSecretMatch := strings.TrimSpace(secretMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Docusign,
Raw: []byte(resIDMatch),
Redacted: resIDMatch,
RawV2: []byte(resIDMatch + resSecretMatch),
}
// Verify client id and secret pair by using an *undocumented* client_credentials grant type on the oauth2 endpoint.
// If verifier breaks in the future, confirm that the oauth2 endpoint is still accepting the client_credentials grant type.
if verify {
req, err := http.NewRequestWithContext(ctx, "POST", "https://account-d.docusign.com/oauth/token?grant_type=client_credentials", nil)
if err != nil {
continue
}
encodedCredentials := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", resIDMatch, resSecretMatch)))
req.Header.Add("Accept", "application/vnd.docusign+json; version=3")
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedCredentials))
res, err := client.Do(req)
if err != nil {
return nil, errors.WrapPrefix(err, "Error making request", 0)
}
verifiedBodyResponse, err := common.ResponseContainsSubstring(res.Body, "ey")
res.Body.Close()
if err != nil {
return nil, err
}
if err == nil {
if res.StatusCode >= 200 && res.StatusCode < 300 && verifiedBodyResponse {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Docusign
}
func (s Scanner) Description() string {
return "Docusign is an electronic signature and digital transaction management service. Docusign credentials can be used to access and manage digital transactions and documents."
}
================================================
FILE: pkg/detectors/docusign/docusign_integration_test.go
================================================
//go:build detectors
// +build detectors
package docusign
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDocusign_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)
}
integrationKey := testSecrets.MustGetField("DOCUSIGN_INTEGRATION_KEY_ACTIVE")
activeSecret := testSecrets.MustGetField("DOCUSIGN_SECRET_ACTIVE")
inactiveSecret := testSecrets.MustGetField("DOCUSIGN_SECRET_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a docusign id %s and secret %s within", integrationKey, activeSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Docusign,
Verified: true,
RawV2: []byte(integrationKey + activeSecret),
Redacted: integrationKey,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a docusign id %s and secret %s within", integrationKey, inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Docusign,
Verified: false,
RawV2: []byte(integrationKey + inactiveSecret),
Redacted: integrationKey,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Docusign.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Docusign.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/docusign/docusign_test.go
================================================
package docusign
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Basic"
in: "Path"
api_version: v1
docusign_id: "03f36108-730e-9061-ad3f-b77c910b2559"
docusign_secret: "212904c1-60fc-09b2-d615-1849cd748bf4"
base_url: "https://api.example.com/$api_version/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "03f36108-730e-9061-ad3f-b77c910b2559212904c1-60fc-09b2-d615-1849cd748bf4"
)
func TestDocsign_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/doppler/doppler.go
================================================
package doppler
import (
"context"
"encoding/json"
"fmt"
"net/http"
"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/detectorspb"
)
type response struct {
Name string `json:"name"`
Type string `json:"type"`
Workplace struct {
Name string `json:"name"`
} `json:"workplace"`
}
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
//keyPat = regexp.MustCompile(`\b(dp\.pt\.[a-zA-Z0-9]{43})\b`)
keyPat = regexp.MustCompile(`\b(dp\.(?:ct|pt|st(?:\.[a-z0-9\-_]{2,35})?|sa|scim|audit)\.[a-zA-Z0-9]{40,44})\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{
"dp.ct.",
"dp.pt.",
"dp.st",
"dp.sa.",
"dp.scim.",
"dp.audit.",
}
}
// FromData will find and optionally verify Doppler 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Doppler,
Raw: []byte(resMatch),
ExtraData: map[string]string{},
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.doppler.com/v3/me", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
var r response
if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
s1.SetVerificationError(err, resMatch)
continue
}
if r.Type != "" {
s1.ExtraData["key type"] = r.Type
}
if r.Workplace.Name != "" {
s1.ExtraData["workplace"] = r.Workplace.Name
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Doppler
}
func (s Scanner) Description() string {
return "Doppler is a secrets management platform that allows teams to manage and secure environment variables and secrets. Doppler tokens can be used to access and manage these secrets."
}
================================================
FILE: pkg/detectors/doppler/doppler_integration_test.go
================================================
//go:build detectors
// +build detectors
package doppler
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDoppler_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DOPPLER")
inactiveSecret := testSecrets.MustGetField("DOPPLER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a doppler secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Doppler,
Verified: true,
ExtraData: map[string]string{
"key type": "personal",
"workplace": "test",
},
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a doppler secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Doppler,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Doppler.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Doppler.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/doppler/doppler_test.go
================================================
package doppler
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Documo Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Path"
api_version: v1
doppler_secret: "dp.ct.5KE9aLrlMoprKwgigGZl1zJOOMQDcYPTWoTPujmF5Tm3"
base_url: "https://api.example.com/$api_version/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "dp.ct.5KE9aLrlMoprKwgigGZl1zJOOMQDcYPTWoTPujmF5Tm3"
)
func TestDoppler_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/dotdigital/dotdigital.go
================================================
package dotdigital
import (
"context"
"fmt"
"io"
"net/http"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
emailPat = regexp.MustCompile(`\b(apiuser-[a-z0-9]{12}@apiconnector.com)\b`)
passPat = regexp.MustCompile(detectors.PrefixRegex([]string{"pw", "pass"}) + `\b([a-zA-Z0-9\S]{8,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{"@apiconnector.com"}
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Dotdigital 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)
var uniqueEmails, uniquePasswords = make(map[string]struct{}), make(map[string]struct{})
for _, matches := range emailPat.FindAllStringSubmatch(dataStr, -1) {
uniqueEmails[matches[1]] = struct{}{}
}
for _, matches := range passPat.FindAllStringSubmatch(dataStr, -1) {
uniquePasswords[matches[1]] = struct{}{}
}
for email := range uniqueEmails {
for password := range uniquePasswords {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Dotdigital,
Raw: []byte(email),
RawV2: []byte(email + password),
}
if verify {
client := s.getClient()
isVerified, verificationErr := verifyMatch(ctx, client, email, password)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
if s1.Verified {
// Once the email is verified, we can stop checking other passwords for it.
break
}
}
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, email, pass string) (bool, error) {
// Reference: https://developer.dotdigital.com/reference/get-account-information
timeout := 10 * time.Second
client.Timeout = timeout
url := "https://r1-api.dotdigital.com/v2/account-info"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return false, err
}
req.SetBasicAuth(email, pass)
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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Dotdigital
}
func (s Scanner) Description() string {
return "Dotdigital is an email marketing automation platform. API keys can be used to access and manage email campaigns and related data."
}
================================================
FILE: pkg/detectors/dotdigital/dotdigital_integration_test.go
================================================
//go:build detectors
// +build detectors
package dotdigital
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDotdigital_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
email := testSecrets.MustGetField("DOTDIGITAL_EMAIL")
password := testSecrets.MustGetField("DOTDIGITAL_PASSWORD")
inactivePassword := testSecrets.MustGetField("DOTDIGITAL_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dotdigital user %s within dotdigital pass %s", email, password)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dotdigital,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dotdigital user %s within dotdigital pass %s but not valid ", email, inactivePassword)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dotdigital,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Dotdigital.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", "primarySecret")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Dotdigital.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/dotdigital/dotdigital_test.go
================================================
package dotdigital
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
api:
auth_type: "Basic"
dotdigital_email: "apiuser-trq6zw9mmdlt@apiconnector.com"
dotdigital_password: "N{w44mqa'2si(zY8"
base_url: "https://api.example.com/$api_version/example"
response_code: 200
# Notes:
# - The above credentials should only be used in a secure environment.
`
secrets = []string{"apiuser-trq6zw9mmdlt@apiconnector.comN{w44mqa'2si(zY8"}
)
func TestDotdigital_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: secrets,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/dovico/dovico.go
================================================
package dovico
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"dovico"}) + `\b([0-9a-z]{32}\.[0-9a-z]{1,}\b)`)
userPat = regexp.MustCompile(detectors.PrefixRegex([]string{"dovico"}) + `\b([0-9a-z]{32}\.[0-9a-z]{1,}\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{"dovico"}
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Dovico 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)
uniqueKeys := make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
uniqueUserKeys := make(map[string]struct{})
for _, matches := range userPat.FindAllStringSubmatch(dataStr, -1) {
uniqueUserKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
for userKey := range uniqueUserKeys {
if key == userKey {
continue // Skip if ID and secret are the same.
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Dovico,
Raw: []byte(key),
RawV2: []byte(fmt.Sprintf("%s:%s", key, userKey)),
}
if verify {
client := s.getClient()
isVerified, err := verifyMatch(ctx, client, key, userKey)
s1.Verified = isVerified
s1.SetVerificationError(err, key, userKey)
}
results = append(results, s1)
// Credentials have 1:1 mapping so we can stop checking other user keys once it is verified
if s1.Verified {
break
}
}
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, key, user string) (bool, error) {
// Reference: https://timesheet.dovico.com/developer/API_doc/#t=API_Overview.html
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.dovico.com/employees/?version=7", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf(`WRAP access_token="client=%s&user_token=%s"`, key, user))
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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Dovico
}
func (s Scanner) Description() string {
return "Dovico is a time tracking and project management service. Dovico keys can be used to access and manage time tracking and project data."
}
================================================
FILE: pkg/detectors/dovico/dovico_integration_test.go
================================================
//go:build detectors
// +build detectors
package dovico
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDovico_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DOVICO_CLIENT")
user := testSecrets.MustGetField("DOVICO_USER")
inactiveSecret := testSecrets.MustGetField("DOVICO_CLIENT_INACTIVE")
inactiveUser := testSecrets.MustGetField("DOVICO_USER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dovico secret %s within dovico user %s ", secret, user)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dovico,
Verified: true,
},
{
DetectorType: detectorspb.DetectorType_Dovico,
Verified: false,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dovico secret %s within dovico user %s but not valid", inactiveSecret, inactiveUser)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dovico,
Verified: false,
},
{
DetectorType: detectorspb.DetectorType_Dovico,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Dovico.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])
}
got[i].Raw = nil
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Dovico.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/dovico/dovico_test.go
================================================
package dovico
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Token"
in: "Header"
api_version: v1
dovico_user: "ntb4fnhk5iot7hzbfjw08jm661iocdd4.3ws4ol"
dovico_token: "nuhkw7nsrybuvmetium29a6oajxr3xdg.sbpi6e"
base_url: "https://api.example.com/$api_version/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secrets = []string{
"nuhkw7nsrybuvmetium29a6oajxr3xdg.sbpi6e:ntb4fnhk5iot7hzbfjw08jm661iocdd4.3ws4ol",
"ntb4fnhk5iot7hzbfjw08jm661iocdd4.3ws4ol:nuhkw7nsrybuvmetium29a6oajxr3xdg.sbpi6e",
}
)
func TestDovico_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: secrets,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/dronahq/dronahq.go
================================================
package dronahq
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"dronahq"}) + `\b([a-z0-9]{50})\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{"dronahq"}
}
// FromData will find and optionally verify DronaHQ 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_DronaHQ,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://plugin.api.dronahq.com/users/?tokenkey=%s", resMatch), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_DronaHQ
}
func (s Scanner) Description() string {
return "DronaHQ is a platform for building internal tools and applications. DronaHQ keys can be used to access and manage these tools and applications."
}
================================================
FILE: pkg/detectors/dronahq/dronahq_integration_test.go
================================================
//go:build detectors
// +build detectors
package dronahq
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDronaHQ_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DRONAHQ")
inactiveSecret := testSecrets.MustGetField("DRONAHQ_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dronahq secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DronaHQ,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dronahq secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DronaHQ,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("DronaHQ.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("DronaHQ.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/dronahq/dronahq_test.go
================================================
package dronahq
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
api_version: v1
dronahq_secret: ""
base_url: "https://api.dronahq.com/$api_version/example?tokenkey=5j5jvhn9hm6qojajn61pe1ccqly424lrd0g41vbh6wwscer3pa"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "5j5jvhn9hm6qojajn61pe1ccqly424lrd0g41vbh6wwscer3pa"
)
func TestDronahq_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/droneci/droneci.go
================================================
package droneci
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"droneci"}) + `\b([a-zA-Z0-9]{32})\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{"droneci"}
}
// FromData will find and optionally verify DroneCI 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_DroneCI,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://cloud.drone.io/api/user", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_DroneCI
}
func (s Scanner) Description() string {
return "DroneCI is a continuous integration service that automates the testing and deployment of applications. DroneCI tokens can be used to access and control CI/CD pipelines."
}
================================================
FILE: pkg/detectors/droneci/droneci_integration_test.go
================================================
//go:build detectors
// +build detectors
package droneci
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDroneCI_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DRONECI_TOKEN")
inactiveSecret := testSecrets.MustGetField("DRONECI_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a droneci secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DroneCI,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a droneci secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DroneCI,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("DroneCI.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("DroneCI.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/droneci/droneci_test.go
================================================
package droneci
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
api_version: v1
droneci_secret: "Kf6ZyWFttCZwO9SqEB94opCHaQ5n00WF"
base_url: "https://api.example.com/$api_version/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "Kf6ZyWFttCZwO9SqEB94opCHaQ5n00WF"
)
func TestDroneCI_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/dropbox/dropbox.go
================================================
package dropbox
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"dropbox"}) + `\b(sl\.(u\.)?[A-Za-z0-9\-\_]{130,})\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{"dropbox", "sl."}
}
// FromData will find and optionally verify Dropbox 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)
var uniqueKeys = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[matches[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Dropbox,
Raw: []byte(key),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyDropboxToken(ctx, client, key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
if s1.Verified {
s1.AnalysisInfo = map[string]string{"token": key}
}
}
results = append(results, s1)
}
return
}
func verifyDropboxToken(ctx context.Context, client *http.Client, key string) (bool, error) {
// Reference: https://www.dropbox.com/developers/documentation/http/documentation
url := "https://api.dropboxapi.com/2/users/get_current_account"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
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.StatusUnauthorized, http.StatusBadRequest:
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, fmt.Errorf("failed to read response body: %w", err)
}
body := string(bodyBytes)
if strings.Contains(body, "missing_scope") ||
strings.Contains(body, "does not have the required scope") {
return true, nil // The token is valid but lacks the required scope
}
if strings.Contains(body, "invalid_access_token") ||
strings.Contains(body, "expired_access_token") {
return false, nil // The token is invalid or expired
}
return false, fmt.Errorf("unexpected status code: %d, body: %s", res.StatusCode, body)
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Dropbox
}
func (s Scanner) Description() string {
return "Dropbox is a file hosting service that offers cloud storage, file synchronization, personal cloud, and client software. Dropbox API keys can be used to access and manage files and folders in a Dropbox account."
}
================================================
FILE: pkg/detectors/dropbox/dropbox_integration_test.go
================================================
//go:build detectors
// +build detectors
package dropbox
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDropbox_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DROPBOX")
secretInactive := testSecrets.MustGetField("DROPBOX_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dropbox secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dropbox,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dropbox secret %s within", secretInactive)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dropbox,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Dropbox.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
for i := range got {
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Dropbox.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/dropbox/dropbox_test.go
================================================
package dropbox
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
api_version: v1
dropbox_secret: "sl.4ihqlizKRm9J8tJvdBUecLPfYunjh3Nx73cUBGcRKpTFxRny3cYKdaQdzVF_rBIEO9emJaHyRWeM_tm5pYJFTc1TwYjM2fHlhSdhKkzHJjf5dx86fUlaO_eKY9r4ijZ8eD"
base_url: "https://api.example.com/$api_version/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "sl.4ihqlizKRm9J8tJvdBUecLPfYunjh3Nx73cUBGcRKpTFxRny3cYKdaQdzVF_rBIEO9emJaHyRWeM_tm5pYJFTc1TwYjM2fHlhSdhKkzHJjf5dx86fUlaO_eKY9r4ijZ8eD"
)
func TestDropBox_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/duply/duply.go
================================================
package duply
import (
"context"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"duply"}) + `\b([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"duply"}) + `\b([0-9A-Z]{7}-[0-9A-Z]{7}-[0-9A-Z]{7}-[0-9A-Z]{7})\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{"duply"}
}
// FromData will find and optionally verify Duply 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
resIdMatch := strings.TrimSpace(idMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Duply,
Raw: []byte(resMatch),
}
if verify {
timeout := 10 * time.Second
client.Timeout = timeout
req, err := http.NewRequestWithContext(ctx, "GET", "https://gen.duply.co/v1/usage", nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.SetBasicAuth(resIdMatch, resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Duply
}
func (s Scanner) Description() string {
return "An API for generating images. API keys can fetch and create images."
}
================================================
FILE: pkg/detectors/duply/duply_integration_test.go
================================================
//go:build detectors
// +build detectors
package duply
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDuply_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DUPLY")
id := testSecrets.MustGetField("DUPLY_USER")
inactiveSecret := testSecrets.MustGetField("DUPLY_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a duply secret %s within duply %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Duply,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a duply secret %s within duply %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Duply,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Duply.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Duply.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/duply/duply_test.go
================================================
package duply
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Basic"
in: "Header"
api_version: v1
duply_id: "JN9YXKN-2OB6UTI-VTN7DX8-FIZZM7P"
duply_secret: "24cc4537-f4ea-b9de-7369-41481c6e914f"
base_url: "https://api.example.com/$api_version/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "24cc4537-f4ea-b9de-7369-41481c6e914f"
)
func TestDuply_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/dwolla/dwolla.go
================================================
package dwolla
import (
"context"
b64 "encoding/base64"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"dwolla"}) + `\b([a-zA-Z-0-9]{50})\b`)
secretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"dwolla"}) + `\b([a-zA-Z-0-9]{50})\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{"dwolla"}
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Dwolla 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)
uniqueIDs := make(map[string]struct{})
for _, matches := range idPat.FindAllStringSubmatch(dataStr, -1) {
uniqueIDs[matches[1]] = struct{}{}
}
uniqueSecrets := make(map[string]struct{})
for _, matches := range secretPat.FindAllStringSubmatch(dataStr, -1) {
uniqueSecrets[matches[1]] = struct{}{}
}
for id := range uniqueIDs {
for secret := range uniqueSecrets {
if id == secret {
continue // Skip if ID and secret are the same.
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Dwolla,
Raw: []byte(id),
RawV2: []byte(id + secret),
}
if verify {
client := s.getClient()
isVerified, err := verifyMatch(ctx, client, id, secret)
s1.Verified = isVerified
s1.SetVerificationError(err, id, secret)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, id, secret string) (bool, error) {
data := fmt.Sprintf("%s:%s", id, secret)
encoded := b64.StdEncoding.EncodeToString([]byte(data))
payload := strings.NewReader("grant_type=client_credentials")
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api-sandbox.dwolla.com/token", payload)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encoded))
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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Dwolla
}
func (s Scanner) Description() string {
return "Dwolla is a payment services provider that allows businesses to send, receive, and facilitate payments. Dwolla API keys can be used to access and manage these payment services."
}
================================================
FILE: pkg/detectors/dwolla/dwolla_integration_test.go
================================================
//go:build detectors
// +build detectors
package dwolla
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/detectorspb"
)
func TestDwolla_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
id := testSecrets.MustGetField("DWOLLA_ID")
secret := testSecrets.MustGetField("DWOLLA_SECRET")
inactiveSecret := testSecrets.MustGetField("DWOLLA_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dwolla secret %s within dwolla id %s but verified", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dwolla,
Verified: false,
},
{
DetectorType: detectorspb.DetectorType_Dwolla,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dwolla secret %s within dwolla id %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dwolla,
Verified: false,
},
{
DetectorType: detectorspb.DetectorType_Dwolla,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Dwolla.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", "primarySecret")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Dwolla.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/dwolla/dwolla_test.go
================================================
package dwolla
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Basic"
in: "Header"
api_version: v1
dwolla_id: "MvkLktYDS7PSE0xRMHIYBKrAjXruEk5P1VrJUUGtgspa3KTi6r"
dwolla_secret: "q3DZbY7iviUpewfCHEpK1I51G8XW63GuLuJyAIEqOFtEB1qlg1"
base_url: "https://api.example.com/$api_version/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secrets = []string{
"MvkLktYDS7PSE0xRMHIYBKrAjXruEk5P1VrJUUGtgspa3KTi6rq3DZbY7iviUpewfCHEpK1I51G8XW63GuLuJyAIEqOFtEB1qlg1",
"q3DZbY7iviUpewfCHEpK1I51G8XW63GuLuJyAIEqOFtEB1qlg1MvkLktYDS7PSE0xRMHIYBKrAjXruEk5P1VrJUUGtgspa3KTi6r",
}
)
func TestDwollaPattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: secrets,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/dynalist/dynalist.go
================================================
package dynalist
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"dynalist"}) + `\b([a-zA-Z0-9-_]{128})\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{"dynalist"}
}
// FromData will find and optionally verify Dynalist 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Dynalist,
Raw: []byte(resMatch),
}
if verify {
payload := strings.NewReader(fmt.Sprintf(`{"token": "%s"}`, resMatch))
req, err := http.NewRequestWithContext(ctx, "POST", "https://dynalist.io/api/v1/file/list", payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
bodyBytes, err := io.ReadAll(res.Body)
if err == nil {
bodyString := string(bodyBytes)
validResponse := strings.Contains(bodyString, `"_code":"Ok"`)
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
if validResponse {
s1.Verified = true
} else {
s1.Verified = false
}
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Dynalist
}
func (s Scanner) Description() string {
return "Dynalist is a web-based outlining app that allows users to create and manage hierarchical lists. Dynalist API tokens can be used to access and manipulate these lists programmatically."
}
================================================
FILE: pkg/detectors/dynalist/dynalist_integration_test.go
================================================
//go:build detectors
// +build detectors
package dynalist
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDynalist_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DYNALIST")
inactiveSecret := testSecrets.MustGetField("DYNALIST_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dynalist secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dynalist,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dynalist secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dynalist,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Dynalist.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Dynalist.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/dynalist/dynalist_test.go
================================================
package dynalist
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Token"
in: "Body"
api_version: v1
dynalist_secret: "60l0XYOX_VZrJsbpid4TllHwdek_3NXxgKz_DkO3lrw_B8aHxSov-TOPojCMtBED8q4awfqsMdNcOkCxrmqkbDQW2dJ8lukGTgJZBqfPQKOYujZZBXKZng3SXM-huIRM"
base_url: "https://api.example.com/$api_version/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "60l0XYOX_VZrJsbpid4TllHwdek_3NXxgKz_DkO3lrw_B8aHxSov-TOPojCMtBED8q4awfqsMdNcOkCxrmqkbDQW2dJ8lukGTgJZBqfPQKOYujZZBXKZng3SXM-huIRM"
)
func TestDynalist_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/dyspatch/dyspatch.go
================================================
package dyspatch
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"dyspatch"}) + `\b([A-Z0-9]{52})\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{"dyspatch"}
}
// FromData will find and optionally verify Dyspatch 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Dyspatch,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.dyspatch.io/templates", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/vnd.dyspatch.2020.11+json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
continue
}
body := string(bodyBytes)
validResponse := strings.Contains(body, "limited_usage") || strings.Contains(body, "data")
if validResponse {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Dyspatch
}
func (s Scanner) Description() string {
return "Dyspatch is a platform for managing and sending transactional emails. Dyspatch API keys can be used to access and manage email templates and sending operations."
}
================================================
FILE: pkg/detectors/dyspatch/dyspatch_integration_test.go
================================================
//go:build detectors
// +build detectors
package dyspatch
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestDyspatch_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("DYSPATCH_TOKEN")
inactiveSecret := testSecrets.MustGetField("DYSPATCH_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dyspatch secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dyspatch,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a dyspatch secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Dyspatch,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Dyspatch.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Dyspatch.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/dyspatch/dyspatch_test.go
================================================
package dyspatch
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
api_version: v1
dyspatch_secret: "RLZTXG010RHW7FCSAEX72TPRJS1JU1PU0PVSWFF6HZQOUEVY5MFN"
base_url: "https://api.example.com/$api_version/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "RLZTXG010RHW7FCSAEX72TPRJS1JU1PU0PVSWFF6HZQOUEVY5MFN"
)
func TestDyspatch_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/eagleeyenetworks/eagleeyenetworks.go
================================================
package eagleeyenetworks
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"eagleeyenetworks"}) + `\b([a-zA-Z0-9]{15})\b`)
email = regexp.MustCompile(detectors.PrefixRegex([]string{"eagleeyenetworks"}) + `\b([a-zA-Z0-9]{3,20}@[a-zA-Z0-9]{2,12}.[a-zA-Z0-9]{2,5})\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{"eagleeyenetworks"}
}
// FromData will find and optionally verify EagleEyeNetworks 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
emailMatches := email.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, emailMatch := range emailMatches {
resEmailPatMatch := strings.TrimSpace(emailMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_EagleEyeNetworks,
Raw: []byte(resMatch),
}
if verify {
payload := strings.NewReader(fmt.Sprintf(`{"username": "%s", "password": "%s"}`, resEmailPatMatch, resMatch))
req, err := http.NewRequestWithContext(ctx, "POST", "https://login.eagleeyenetworks.com/g/aaa/authenticate", payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_EagleEyeNetworks
}
func (s Scanner) Description() string {
return "Eagle Eye Networks provides cloud-based video surveillance solutions. The credentials can be used to access and manage surveillance data."
}
================================================
FILE: pkg/detectors/eagleeyenetworks/eagleeyenetworks_integration_test.go
================================================
//go:build detectors
// +build detectors
package eagleeyenetworks
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestEagleEyeNetworks_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("EAGLEEYENETWORKS")
email := testSecrets.MustGetField("EAGLEEYENETWORKS_USER")
inactiveSecret := testSecrets.MustGetField("EAGLEEYENETWORKS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a eagleeyenetworks secret %s within eagleeyenetworks %s", secret, email)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EagleEyeNetworks,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a eagleeyenetworks secret %s within eagleeyenetworks %s but not valid", inactiveSecret, email)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EagleEyeNetworks,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("EagleEyeNetworks.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("EagleEyeNetworks.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/eagleeyenetworks/eagleeyenetworks_test.go
================================================
package eagleeyenetworks
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Token"
in: "Body"
api_version: v1
eagleeyenetworks_email: "test08@eagleeyenetworks.com"
eagleeyenetworks_secret: "Y6YWq0NfYgyJCL0"
base_url: "https://api.example.com/$api_version/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "Y6YWq0NfYgyJCL0"
)
func TestEagleEyeNetworks_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/easyinsight/easyinsight.go
================================================
package easyinsight
import (
"context"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"easyinsight", "easy-insight", "key"}) + `\b([0-9a-zA-Z]{20})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"easyinsight", "easy-insight", "id"}) + `\b([a-zA-Z0-9]{20})\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{"easyinsight", "easy-insight"}
}
// FromData will find and optionally verify EasyInsight 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)
var keyMatches, idMatches = make(map[string]struct{}), make(map[string]struct{})
// get unique key and id matches
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
keyMatches[matches[1]] = struct{}{}
}
for _, matches := range idPat.FindAllStringSubmatch(dataStr, -1) {
idMatches[matches[1]] = struct{}{}
}
for keyMatch := range keyMatches {
for idMatch := range idMatches {
//as key and id regex are same, the strings captured by both regex will be same.
//avoid processing when key is same as id. This will allow detector to process only different combinations
if keyMatch == idMatch {
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_EasyInsight,
Raw: []byte(keyMatch),
RawV2: []byte(keyMatch + idMatch),
}
if verify {
verified, verificationErr := verifyEasyInsight(ctx, idMatch, keyMatch)
s1.Verified = verified
if verificationErr != nil {
s1.SetVerificationError(verificationErr)
}
}
results = append(results, s1)
// if key id combination is verified, skip other idMatches for that key
if s1.Verified {
break
}
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_EasyInsight
}
func (s Scanner) Description() string {
return "EasyInsight is a business intelligence tool that provides data visualization and reporting. EasyInsight API keys can be used to access and manage data within the platform."
}
func verifyEasyInsight(ctx context.Context, id, key string) (bool, error) {
// docs: https://www.easy-insight.com/api/users.html
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.easy-insight.com/app/api/users.json", nil)
if err != nil {
return false, err
}
// add required headers to the request
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
// set basic auth for the request
req.SetBasicAuth(id, key)
res, reqErr := client.Do(req)
if reqErr != nil {
return false, reqErr
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
// id, key verified
case http.StatusOK:
return true, nil
// id, key unverified
case http.StatusUnauthorized:
return false, nil
// something invalid
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
================================================
FILE: pkg/detectors/easyinsight/easyinsight_integration_test.go
================================================
//go:build detectors
// +build detectors
package easyinsight
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestEasyInsight_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("EASYINSIGHT")
inactiveSecret := testSecrets.MustGetField("EASYINSIGHT_INACTIVE")
id := testSecrets.MustGetField("EASYINSIGHT_ID")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a easyinsight secret %s within easyid %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EasyInsight,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a easyinsight secret %s within easyid %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EasyInsight,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("EasyInsight.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("EasyInsight.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/easyinsight/easyinsight_test.go
================================================
package easyinsight
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 (
validKeyPattern = "987ahjjdasgUcaaraAdd"
validIDPattern = "poiuy76RaEf90ertgh0K"
// this should result in 4 combinations
complexPattern = `easyinsight credentials
these credentials are for testing a pattern
key: A876AcaraTsaAKcae09a
id: chECk12345ChecK12345
-------------------------
second credentials:
key: B874CDaraTsaAKVBe08A
id: CHECK12345ChecK09876`
invalidPattern = "poiuy76=a_$90ertgh0K"
)
func TestEasyInsight_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: fmt.Sprintf("easyinsight key = '%s' easy-insight id = '%s", validKeyPattern, validIDPattern),
want: []string{validKeyPattern + validIDPattern, validIDPattern + validKeyPattern},
},
{
name: "valid pattern - complex",
input: fmt.Sprintf("easyinsight token = '%s'", complexPattern),
want: []string{
"A876AcaraTsaAKcae09achECk12345ChecK12345",
"A876AcaraTsaAKcae09aCHECK12345ChecK09876",
"B874CDaraTsaAKVBe08ACHECK12345ChecK09876",
"B874CDaraTsaAKVBe08AchECk12345ChecK12345",
},
},
{
name: "valid pattern - out of prefix range",
input: fmt.Sprintf("easyinsight key and id keyword is not close to the real token = '%s|%s'", validKeyPattern, validIDPattern),
want: nil,
},
{
name: "invalid pattern",
input: fmt.Sprintf("easyinsight = '%s|%s'", invalidPattern, invalidPattern),
want: nil,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/ecostruxureit/ecostruxureit.go
================================================
package ecostruxureit
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ecostruxureit"}) + `\b(AK1[0-9a-zA-Z\/]{50,55})\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{"ecostruxureit"}
}
// FromData will find and optionally verify EcoStruxureIT 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_EcoStruxureIT,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.ecostruxureit.com/rest/v1/organizations", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_EcoStruxureIT
}
func (s Scanner) Description() string {
return "EcoStruxure IT is a cloud-based platform that provides IT infrastructure management. EcoStruxure IT API keys can be used to access and manage IT infrastructure data and operations."
}
================================================
FILE: pkg/detectors/ecostruxureit/ecostruxureit_integration_test.go
================================================
//go:build detectors
// +build detectors
package ecostruxureit
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestEcoStruxureIT_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ECOSTRUXUREIT")
inactiveSecret := testSecrets.MustGetField("ECOSTRUXUREIT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a ecostruxureit secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EcoStruxureIT,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a ecostruxureit secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EcoStruxureIT,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("EcoStruxureIT.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("EcoStruxureIT.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/ecostruxureit/ecostruxureit_test.go
================================================
package ecostruxureit
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
api_version: v1
ecostruxureit_secret: "AK1CI5QsL1zvRE5KBX/uL9KuiZOJq27GwcJu4fV/xyTJcYCrYP0ykE"
base_url: "https://api.example.com/$api_version/example"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "AK1CI5QsL1zvRE5KBX/uL9KuiZOJq27GwcJu4fV/xyTJcYCrYP0ykE"
)
func TestEcostruxureit_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/edamam/edamam.go
================================================
package edamam
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"edamam"}) + `\b([0-9a-z]{32})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"edamam"}) + `\b([0-9a-z]{8})\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{"edamam"}
}
// FromData will find and optionally verify Edamam 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
resId := strings.TrimSpace(idMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Edamam,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resId),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.edamam.com/auto-complete?app_id=%s&app_key=%s&q=%s", resId, resMatch, ""), nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Edamam
}
func (s Scanner) Description() string {
return "Edamam provides nutrition analysis and diet recommendations. Edamam API keys can be used to access and modify nutrition data and perform diet analysis."
}
================================================
FILE: pkg/detectors/edamam/edamam_integration_test.go
================================================
//go:build detectors
// +build detectors
package edamam
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestEdamam_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("EDAMAM")
id := testSecrets.MustGetField("EDAMAM_ID")
inactiveSecret := testSecrets.MustGetField("EDAMAM_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a edamam secret %s within edamam id %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Edamam,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a edamam secret %s within edamam id %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Edamam,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Edamam.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])
}
got[i].Raw = nil
if len(got[i].RawV2) == 0 {
t.Fatalf("no raw v2 secret present: \n %+v", got[i])
}
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Edamam.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/edamam/edamam_test.go
================================================
package edamam
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
api_version: v1
edamam_id: "1vsqsubh"
edamam_secret: "e3at3vut4x27aq5wpkjmivjt9kq5cune"
base_url: "https://api.example.com/$api_version/example"
query: "app_id=$edamam_id&app_key=$edamam_secret"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "e3at3vut4x27aq5wpkjmivjt9kq5cune1vsqsubh"
)
func TestEdamam_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/edenai/edenai.go
================================================
package edenai
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"edenai"}) + `\b([a-zA-Z0-9]{36}.[a-zA-Z0-9]{92}.[a-zA-Z0-9_]{43})\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{"edenai"}
}
// FromData will find and optionally verify EdenAI 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_EdenAI,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.edenai.run/v1/automl/text/project", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_EdenAI
}
func (s Scanner) Description() string {
return "EdenAI provides a unified API to access multiple AI engines. EdenAI API keys can be used to access and utilize these AI services."
}
================================================
FILE: pkg/detectors/edenai/edenai_integration_test.go
================================================
//go:build detectors
// +build detectors
package edenai
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestEdenAI_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("EDENAI")
inactiveSecret := testSecrets.MustGetField("EDENAI_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a edenai secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EdenAI,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a edenai secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EdenAI,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("EdenAI.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("EdenAI.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/edenai/edenai_test.go
================================================
package edenai
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
api_version: v1
edenai_secret: "CQcxzQhT70xdDr8J6zGDpTY4Iv3ro10k1YMG}XLr0DyMXgnxMCqx4m92bgOK5QkBZJJNSoOHk8y6yEuoIu6MBb5I12Jbrjw9TpMWUf8dgxSlFyvFpyUOz5A3gvJu926a4F17oRzQpAfBAjGpL91ZxNtZ5uDy50MNnh1VgadWFnRzR"
base_url: "https://api.example.com/$api_version/example"
query: ""
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "CQcxzQhT70xdDr8J6zGDpTY4Iv3ro10k1YMG}XLr0DyMXgnxMCqx4m92bgOK5QkBZJJNSoOHk8y6yEuoIu6MBb5I12Jbrjw9TpMWUf8dgxSlFyvFpyUOz5A3gvJu926a4F17oRzQpAfBAjGpL91ZxNtZ5uDy50MNnh1VgadWFnRzR"
)
func TestEdenai_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/eightxeight/eightxeight.go
================================================
package eightxeight
import (
"context"
"fmt"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"8x8"}) + `\b([a-zA-Z0-9]{43})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"8x8"}) + `\b([a-zA-Z0-9_]{18,30})\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{"8x8"}
}
// FromData will find and optionally verify EightxEight 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
resIdMatch := strings.TrimSpace(idMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_EightxEight,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch),
}
if verify {
timeout := 10 * time.Second
client.Timeout = timeout
payload := strings.NewReader(`{"source":"abcde","destination":"+6512345678","text":"Hello World!","encoding":"AUTO"}`)
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("https://sms.8x8.com/api/v1/subaccounts/%s/messages", resIdMatch), payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_EightxEight
}
func (s Scanner) Description() string {
return "8x8 is a provider of cloud-based communication services including voice, video, chat, and contact center solutions."
}
================================================
FILE: pkg/detectors/eightxeight/eightxeight_integration_test.go
================================================
//go:build detectors
// +build detectors
package eightxeight
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestEightxEight_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("EIGHTXEIGHT")
id := testSecrets.MustGetField("EIGHTXEIGHT_ID")
inactiveSecret := testSecrets.MustGetField("EIGHTXEIGHT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a 8x8 secret %s within 8x8 %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EightxEight,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a 8x8 secret %s within 8x8 %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EightxEight,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("EightxEight.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("EightxEight.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/eightxeight/eightxeight_test.go
================================================
package eightxeight
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
api_version: v1
8x8_id: "ByvWSRLcNhS_bgLBjD4hAhUvkWLz"
8x8_secret: "LiE1BOtWbU7YucNYPnXNG0LIFlkfWcMt8KLBu1MfjeS"
base_url: "https://api.example.com/$api_version/example"
query: ""
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "LiE1BOtWbU7YucNYPnXNG0LIFlkfWcMt8KLBu1MfjeSByvWSRLcNhS_bgLBjD4hAhUvkWLz"
)
func TestEightXEight_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/elasticemail/elasticemail.go
================================================
package elasticemail
import (
"context"
"encoding/json"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"elastic"}) + `\b([A-Za-z0-9_-]{96})\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{"elasticemail"}
}
// FromData will find and optionally verify ElasticEmail 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ElasticEmail,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.elasticemail.com/v2/account/profileoverview?apikey="+resMatch, nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
data, readErr := io.ReadAll(res.Body)
res.Body.Close()
if readErr == nil {
var ResVar struct {
Success bool `json:"success"`
}
if err := json.Unmarshal(data, &ResVar); err == nil {
if ResVar.Success {
s1.Verified = true
}
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ElasticEmail
}
func (s Scanner) Description() string {
return "ElasticEmail is an email marketing service. ElasticEmail API keys can be used to send emails, manage contacts, and access other features of the service."
}
================================================
FILE: pkg/detectors/elasticemail/elasticemail_integration_test.go
================================================
//go:build detectors
// +build detectors
package elasticemail
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestElasticEmail_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ELASTICEMAIL_TOKEN")
inactiveSecret := testSecrets.MustGetField("ELASTICEMAIL_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a elasticemail secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ElasticEmail,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a elasticemail secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ElasticEmail,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("ElasticEmail.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("ElasticEmail.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/elasticemail/elasticemail_test.go
================================================
package elasticemail
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
api_version: v1
elasticemail_secret: "KjzCaS0dOHBFkH6ljFkQp353jV8FH5Fgmo9-t9Bgl2iP1btjXEEaGwOPVnR8LZFSksLpL4kwxUXOFJBGwz6xBbVeJIR8K17p"
base_url: "https://api.example.com/$api_version/example"
query: "apiKey=$elasticemail_secret"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "KjzCaS0dOHBFkH6ljFkQp353jV8FH5Fgmo9-t9Bgl2iP1btjXEEaGwOPVnR8LZFSksLpL4kwxUXOFJBGwz6xBbVeJIR8K17p"
)
func TestElasticEmail_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/elevenlabs/v1/elevenlabs.go
================================================
package elevenlabs
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/detectorspb"
)
type Scanner struct {
client *http.Client
}
func (Scanner) Version() int { return 1 }
type UserRes struct {
Subscription struct {
Tier string `json:"tier"`
} `json:"subscription"`
Name string `json:"first_name"`
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`(?i)(?:elevenlabs|xi-api-key|el|token|key)[^\.].{0,40}[ =:'"]+([a-f0-9]{32})\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{"elevenlabs", "xi-api-key", "xi_api_key"}
}
// FromData will find and optionally verify Elevenlabs 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 match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ElevenLabs,
Raw: []byte(match),
ExtraData: map[string]string{
"version": "1",
"rotation_guide": "https://howtorotate.com/docs/tutorials/elevenlabs/",
},
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, userResponse, verificationErr := verifyMatch(ctx, client, match)
s1.Verified = isVerified
if userResponse != nil {
s1.ExtraData["Name"] = userResponse.Name
s1.ExtraData["Tier"] = userResponse.Subscription.Tier
}
s1.SetVerificationError(verificationErr, match)
if s1.Verified {
s1.AnalysisInfo = map[string]string{
"key": match,
}
}
}
results = append(results, s1)
}
return
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, *UserRes, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.elevenlabs.io/v1/user", nil)
if err != nil {
return false, nil, err
}
req.Header.Set("xi-api-key", token)
res, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
// If the endpoint returns useful information, we can return it as a map.
var userResponse UserRes
if err = json.NewDecoder(res.Body).Decode(&userResponse); err != nil {
return false, nil, err
}
return true, &userResponse, nil
case http.StatusBadRequest, http.StatusUnauthorized:
// The secret is determinately not verified (nothing to do)
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ElevenLabs
}
func (s Scanner) Description() string {
return "Elevenlabs is an AI-driven voice synthesis platform. Elevenlabs API keys can be used to access and manipulate voice synthesis features and services."
}
================================================
FILE: pkg/detectors/elevenlabs/v1/elevenlabs_integration_test.go
================================================
//go:build detectors
// +build detectors
package elevenlabs
import (
"context"
"testing"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
)
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)
}
}
})
}
}
================================================
FILE: pkg/detectors/elevenlabs/v1/elevenlabs_test.go
================================================
package elevenlabs
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
func TestElevenlabs_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "typical pattern",
input: "XI_API_KEY = 'b41b9d78aefb8c7c6cf9ebf01231340b'",
want: []string{"b41b9d78aefb8c7c6cf9ebf01231340b"},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/elevenlabs/v2/elevenlabs.go
================================================
package elevenlabs
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/detectorspb"
)
type Scanner struct {
client *http.Client
}
func (Scanner) Version() int { return 2 }
type UserRes struct {
Subscription struct {
Tier string `json:"tier"`
} `json:"subscription"`
Name string `json:"first_name"`
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b((?:sk)_[a-f0-9]{48})\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{"elevenlabs", "xi-api-key", "xi_api_key"}
}
// FromData will find and optionally verify Elevenlabs 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 match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ElevenLabs,
Raw: []byte(match),
ExtraData: map[string]string{"version": "2"},
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, userResponse, verificationErr := verifyMatch(ctx, client, match)
s1.Verified = isVerified
if userResponse != nil {
s1.ExtraData["Name"] = userResponse.Name
s1.ExtraData["Tier"] = userResponse.Subscription.Tier
}
s1.SetVerificationError(verificationErr, match)
if s1.Verified {
s1.AnalysisInfo = map[string]string{
"key": match,
}
}
}
results = append(results, s1)
}
return
}
func (s Scanner) Description() string {
return "ElevenLabs is a service that provides API keys for accessing their voice synthesis and other AI-powered tools. These keys can be used to interact with ElevenLabs' services programmatically."
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, *UserRes, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.elevenlabs.io/v1/user", nil)
if err != nil {
return false, nil, err
}
req.Header.Set("xi-api-key", token)
res, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
// If the endpoint returns useful information, we can return it as a map.
var userResponse UserRes
if err = json.NewDecoder(res.Body).Decode(&userResponse); err != nil {
return false, nil, err
}
return true, &userResponse, nil
case http.StatusBadRequest, http.StatusUnauthorized:
// The secret is determinately not verified (nothing to do)
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ElevenLabs
}
================================================
FILE: pkg/detectors/elevenlabs/v2/elevenlabs_integration_test.go
================================================
//go:build detectors
// +build detectors
package elevenlabs
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/detectorspb"
)
func TestElevenlabs_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("ELEVENLABS")
inactiveSecret := testSecrets.MustGetField("ELEVENLABS_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a elevenlabs secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ElevenLabs,
Verified: true,
ExtraData: map[string]string{
"version": "2",
"Name": "Trufflesecurity",
"Tier": "free",
},
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a elevenlabs secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ElevenLabs,
Verified: false,
ExtraData: map[string]string{
"version": "2",
},
},
},
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 elevenlabs secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ElevenLabs,
Verified: false,
ExtraData: map[string]string{
"version": "2",
},
},
},
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 elevenlabs secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ElevenLabs,
Verified: false,
ExtraData: map[string]string{
"version": "2",
},
},
},
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("Elevenlabs.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "AnalysisInfo")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Elevenlabs.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/elevenlabs/v2/elevenlabs_test.go
================================================
package elevenlabs
import (
"context"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
"testing"
)
func TestElevenlabs_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "typical pattern",
input: "XI_API_KEY = 'sk_c43667f9bedd46fcff858f09f648d984533645e30f0541df'",
want: []string{"sk_c43667f9bedd46fcff858f09f648d984533645e30f0541df"},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/enablex/enablex.go
================================================
package enablex
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"enablex"}) + `\b([a-zA-Z0-9]{36})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"enablex"}) + `\b([a-z0-9]{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{"enablex"}
}
// FromData will find and optionally verify Enablex 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
tokenPatMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
userPatMatch := strings.TrimSpace(idMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Enablex,
Raw: []byte(tokenPatMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.enablex.io/voice/v1/call", nil)
if err != nil {
continue
}
req.SetBasicAuth(userPatMatch, tokenPatMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Enablex
}
func (s Scanner) Description() string {
return "Enablex is a communication platform offering voice, video, and messaging APIs. Enablex credentials can be used to access and manage communication services provided by Enablex."
}
================================================
FILE: pkg/detectors/enablex/enablex_integration_test.go
================================================
//go:build detectors
// +build detectors
package enablex
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestEnablex_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ENABLEX")
user := testSecrets.MustGetField("ENABLEX_USER")
inactiveSecret := testSecrets.MustGetField("ENABLEX_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a enablex secret %s within enablex %s", secret, user)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Enablex,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a enablex secret %s within enablex %s but not valid", inactiveSecret, user)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Enablex,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Enablex.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Enablex.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/enablex/enablex_test.go
================================================
package enablex
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Basic"
in: "Header"
api_version: v1
enablex_id: "hkhihhsneir2aablmbk55u8f"
enablex_secret: "iSgJYVk9ZhWwgLTH9hyTv1IjqIKUNeX6B623"
base_url: "https://api.example.com/$api_version/example"
query: ""
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "iSgJYVk9ZhWwgLTH9hyTv1IjqIKUNeX6B623"
)
func TestEnableX_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/endorlabs/endorlabs.go
================================================
package endorlabs
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyAndSecretPat = regexp.MustCompile(`\b(endr\+[a-zA-Z0-9-]{16})\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{"endr+"}
}
// FromData will find and optionally verify Endorlabs 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)
keyMatches := make(map[string]struct{})
for _, match := range keyAndSecretPat.FindAllStringSubmatch(dataStr, -1) {
keyMatches[match[1]] = struct{}{}
}
secretMatches := make(map[string]struct{})
for _, match := range keyAndSecretPat.FindAllStringSubmatch(dataStr, -1) {
secretMatches[match[1]] = struct{}{}
}
for key := range keyMatches {
for secret := range secretMatches {
if key == secret { // Minor optimization
continue
}
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_EndorLabs,
Raw: []byte(key),
RawV2: []byte(key + secret),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, extraData, verificationErr := verifyMatch(ctx, client, key, secret)
s1.Verified = isVerified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr, key, secret)
}
results = append(results, s1)
}
}
return
}
func verifyMatch(ctx context.Context, client *http.Client, key, secret string) (bool, map[string]string, error) {
authData := fmt.Sprintf(`{"key":"%s", "secret":"%s"}`, key, secret)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.endorlabs.com/v1/auth/api-key", strings.NewReader(authData))
if err != nil {
return false, nil, err
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
// If the endpoint returns useful information, we can return it as a map.
return true, nil, nil
case http.StatusUnauthorized:
// The secret is determinately not verified (nothing to do)
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_EndorLabs
}
func (s Scanner) Description() string {
return "Endorlabs provides API keys that can be used to authenticate and interact with its services. These keys should be kept confidential to prevent unauthorized access."
}
================================================
FILE: pkg/detectors/endorlabs/endorlabs_integration_test.go
================================================
//go:build detectors
// +build detectors
package endorlabs
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/detectorspb"
)
func TestEndorlabs_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)
}
key := testSecrets.MustGetField("ENDOR_KEY")
secret := testSecrets.MustGetField("ENDOR_SECRET")
inactiveKey := testSecrets.MustGetField("ENDOR_KEY_INACTIVE")
inactiveSecret := testSecrets.MustGetField("ENDOR_SECRET_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a endorlabs key %s and endorlabs secret %s within", key, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EndorLabs,
Verified: true,
},
{
DetectorType: detectorspb.DetectorType_EndorLabs,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a endorlabs key %s and endorlabs secret %s within but not valid", inactiveKey, inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EndorLabs,
Verified: false,
},
{
DetectorType: detectorspb.DetectorType_EndorLabs,
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 endorlabs key %s and endorlabs secret %s within", key, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EndorLabs,
Verified: false,
},
{
DetectorType: detectorspb.DetectorType_EndorLabs,
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 endorlabs key %s and endorlabs secret %s within", key, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EndorLabs,
Verified: false,
},
{
DetectorType: detectorspb.DetectorType_EndorLabs,
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("Endorlabs.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError")
sortOpts := cmpopts.SortSlices(func(a, b []detectors.Result) bool {
return string(a[0].Raw) < string(b[0].Raw)
})
if diff := cmp.Diff(got, tt.want, ignoreOpts, sortOpts); diff != "" {
t.Errorf("Endorlabs.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/endorlabs/endorlabs_test.go
================================================
package endorlabs
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
func TestEndorlabs_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "typical pattern",
input: `
endorlabs_key = 'endr+xTGNjttLb8kVOHZC'
endorlabs_secret = 'endr+gGVYIIrCq1VZTQMW'
`,
want: []string{
"endr+xTGNjttLb8kVOHZC" + "endr+gGVYIIrCq1VZTQMW",
"endr+gGVYIIrCq1VZTQMW" + "endr+xTGNjttLb8kVOHZC",
},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/endpoint_customizer.go
================================================
package detectors
import (
"fmt"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
)
// EndpointSetter implements a sensible default for the SetEndpoints function
// of the EndpointCustomizer interface. A detector can embed this struct to
// gain the functionality.
type EndpointSetter struct {
configuredEndpoints []string
cloudEndpoint string
useCloudEndpoint bool
useFoundEndpoints bool
}
func (e *EndpointSetter) SetConfiguredEndpoints(userConfiguredEndpoints ...string) error {
if len(userConfiguredEndpoints) == 0 {
return fmt.Errorf("at least one endpoint required")
}
deduped := make([]string, 0, len(userConfiguredEndpoints))
for _, endpoint := range userConfiguredEndpoints {
common.AddStringSliceItem(endpoint, &deduped)
}
e.configuredEndpoints = deduped
return nil
}
func (e *EndpointSetter) SetCloudEndpoint(url string) {
e.cloudEndpoint = url
}
func (e *EndpointSetter) UseCloudEndpoint(enabled bool) {
e.useCloudEndpoint = enabled
}
func (e *EndpointSetter) UseFoundEndpoints(enabled bool) {
e.useFoundEndpoints = enabled
}
func (e *EndpointSetter) Endpoints(foundEndpoints ...string) []string {
endpoints := e.configuredEndpoints
if e.useCloudEndpoint && e.cloudEndpoint != "" {
endpoints = append(endpoints, e.cloudEndpoint)
}
if e.useFoundEndpoints {
endpoints = append(endpoints, foundEndpoints...)
}
return endpoints
}
================================================
FILE: pkg/detectors/endpoint_customizer_test.go
================================================
package detectors
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEmbeddedEndpointSetter(t *testing.T) {
type Scanner struct{ EndpointSetter }
var s Scanner
t.Run("useFoundEndpoints is true", func(t *testing.T) {
s.useFoundEndpoints = true
// "baz" is passed to Endpoints, should appear in the result
assert.Equal(t, []string{"baz"}, s.Endpoints("baz"))
})
t.Run("setting configured endpoints", func(t *testing.T) {
// Setting "foo" and "bar"
assert.NoError(t, s.SetConfiguredEndpoints("foo", "bar"))
// Returning error because no endpoints are passed
assert.Error(t, s.SetConfiguredEndpoints())
})
// "foo" and "bar" are added as configured endpoint
t.Run("useFoundEndpoints adds new endpoints", func(t *testing.T) {
// "baz" is added because useFoundEndpoints is true
assert.Equal(t, []string{"foo", "bar", "baz"}, s.Endpoints("baz"))
})
t.Run("useCloudEndpoint is true", func(t *testing.T) {
s.useCloudEndpoint = true
s.cloudEndpoint = "test"
// "test" is added because useCloudEndpoint is true and cloudEndpoint is set
assert.Equal(t, []string{"foo", "bar", "test"}, s.Endpoints())
})
t.Run("disable both foundEndpoints and cloudEndpoint", func(t *testing.T) {
// now disable both useFoundEndpoints and useCloudEndpoint
s.useFoundEndpoints = false
s.useCloudEndpoint = false
// "test" won't be added
assert.Equal(t, []string{"foo", "bar"}, s.Endpoints("test"))
})
t.Run("cloudEndpoint not added when useCloudEndpoint is false", func(t *testing.T) {
s.cloudEndpoint = "new"
// "new" is not added because useCloudEndpoint is false
assert.Equal(t, []string{"foo", "bar"}, s.Endpoints())
})
}
================================================
FILE: pkg/detectors/enigma/enigma.go
================================================
package enigma
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"enigma"}) + `\b([a-zA-Z0-9]{40})\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{"enigma"}
}
// FromData will find and optionally verify Enigma 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Enigma,
Raw: []byte(resMatch),
}
if verify {
payload := strings.NewReader(`{"name":"Enigma Technologies, Inc.","person":{"first_name":"","last_name":""},"address":{"street_address1":"245 5th Ave","street_address2":"","city":"New York","state":"NY","postal_code":"10016"}}`)
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.enigma.com/businesses/match", payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("x-api-key", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Enigma
}
func (s Scanner) Description() string {
return "Enigma is a data intelligence company that provides comprehensive data about businesses. Enigma API keys can be used to access and interact with this data."
}
================================================
FILE: pkg/detectors/enigma/enigma_integration_test.go
================================================
//go:build detectors
// +build detectors
package enigma
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestEnigma_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ENIGMA")
inactiveSecret := testSecrets.MustGetField("ENIGMA_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a enigma secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Enigma,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a enigma secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Enigma,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Enigma.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Enigma.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/enigma/enigma_test.go
================================================
package enigma
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
api_version: v1
enigma_secret: "dkQePsD59DdzfoSuIZ2Po2md3q0ENVnvyIDdxs2E"
base_url: "https://api.example.com/$api_version/example"
query: ""
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "dkQePsD59DdzfoSuIZ2Po2md3q0ENVnvyIDdxs2E"
)
func TestEnigma_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/envoyapikey/envoyapikey.go
================================================
package envoyapikey
import (
"context"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"envoy"}) + `\b([a-zA-Z0-9]{220})\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{"envoy"}
}
// FromData will find and optionally verify Envoy 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_EnvoyApiKey,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.envoy.com/v1/locations", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/vnd.envoy+json; version=3")
req.Header.Add("X-Api-Key", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
// Invalid API keys can also return status code 200, so check for presence of 'status 401' in response body.
if res.StatusCode >= 200 && res.StatusCode < 300 || res.StatusCode == 403 {
if !strings.Contains(string(body), `"status":401`) {
s1.Verified = true
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_EnvoyApiKey
}
func (s Scanner) Description() string {
return "Envoy is a cloud-based platform that provides visitor management solutions. Envoy API keys can be used to access and manage visitor data and other resources within the Envoy platform."
}
================================================
FILE: pkg/detectors/envoyapikey/envoyapikey_integration_test.go
================================================
//go:build detectors
// +build detectors
package envoyapikey
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestEnvoyapikey_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ENVOYAPIKEY")
inactiveSecret := testSecrets.MustGetField("ENVOYAPIKEY_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a envoyapikey secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EnvoyApiKey,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a envoyapikey secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_EnvoyApiKey,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Envoyapikey.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Envoyapikey.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/envoyapikey/envoyapikey_test.go
================================================
package envoyapikey
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
api_version: v1
envoy_secret: "53PbWnxV5h7pZGNmw7U6FL79ithvedz1PWSvhFyJDZbqT5ECihUDeQ4MY6O3qTtKMKNFh2Hc5D54pchSKYyTVKi3nqJITLhZi17uCHJVQKrinOrkGL9IUh6QFjDjN3NcK1HKAimUgcNY2B8meGBfQmQ2QnVhKZcK1E8ldT9w4eb9ihgEwnG2lMjG41k5bZEPos3sJDEJWZ39U2J2Yu6OP8h8AVLw"
base_url: "https://api.example.com/$api_version/example"
query: ""
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "53PbWnxV5h7pZGNmw7U6FL79ithvedz1PWSvhFyJDZbqT5ECihUDeQ4MY6O3qTtKMKNFh2Hc5D54pchSKYyTVKi3nqJITLhZi17uCHJVQKrinOrkGL9IUh6QFjDjN3NcK1HKAimUgcNY2B8meGBfQmQ2QnVhKZcK1E8ldT9w4eb9ihgEwnG2lMjG41k5bZEPos3sJDEJWZ39U2J2Yu6OP8h8AVLw"
)
func TestEnvoyAPIKey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/eraser/eraser.go
================================================
package eraser
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"eraser"}) + `\b([0-9a-zA-Z]{20})\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{"eraser"}
}
// FromData will find and optionally verify Eraser 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 match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Eraser,
Raw: []byte(match),
ExtraData: map[string]string{
"rotation_guide": "https://howtorotate.com/docs/tutorials/eraser/",
},
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, extraData, verificationErr := verifyMatch(ctx, client, match)
s1.Verified = isVerified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr, match)
}
results = append(results, s1)
}
return
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, map[string]string, error) {
// https://docs.eraser.io/reference/generate-diagram-from-eraser-dsl
payload := strings.NewReader("{\"elements\":[{\"type\":\"diagram\"}]}")
url := "https://app.eraser.io/api/render/elements"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, payload)
if err != nil {
return false, nil, err
}
req.Header = http.Header{"Authorization": []string{"Bearer " + token}}
req.Header.Add("content-type", "application/json")
res, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
return true, nil, nil
case http.StatusUnauthorized:
// 401 API token unauthorized
// The secret is determinately not verified (nothing to do)
return false, nil, nil
default:
// 400 The request is missing the 'text' parameter
// 500 Eraser was unable to generate a result
// 503 Service temporarily unavailable. This may be the result of too many requests.
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Eraser
}
func (s Scanner) Description() string {
return "Eraser is a tool used for generating diagrams from DSL. Eraser API tokens can be used to authenticate and interact with the Eraser API."
}
================================================
FILE: pkg/detectors/eraser/eraser_integration_test.go
================================================
//go:build detectors
// +build detectors
package eraser
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/detectorspb"
)
func TestEraser_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("ERASER")
inactiveSecret := testSecrets.MustGetField("ERASER_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a eraser secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Eraser,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a eraser secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Eraser,
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 eraser secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Eraser,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: true,
},
{
name: "found, verified but unexpected api surface",
s: Scanner{client: common.ConstantResponseHttpClient(500, "")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a eraser secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Eraser,
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("Eraser.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Eraser.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/eraser/eraser_test.go
================================================
package eraser
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
func TestEraser_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "typical pattern",
input: "eraser_token = 'KkBmh6TUBIcyFAp20XXa'",
want: []string{"KkBmh6TUBIcyFAp20XXa"},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/etherscan/etherscan.go
================================================
package etherscan
import (
"context"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"etherscan"}) + `\b([0-9A-Z]{34})\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{"etherscan"}
}
// FromData will find and optionally verify Etherscan 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Etherscan,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.etherscan.io/api?module=account&action=balance&address=0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae&tag=latest&apikey="+resMatch, nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
continue
}
body := string(bodyBytes)
if strings.Contains(body, `"OK"`) {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Etherscan
}
func (s Scanner) Description() string {
return "Etherscan is a Block Explorer and Analytics Platform for Ethereum, a decentralized smart contracts platform. Etherscan API keys can be used to access various functionalities provided by Etherscan."
}
================================================
FILE: pkg/detectors/etherscan/etherscan_integration_test.go
================================================
//go:build detectors
// +build detectors
package etherscan
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestEtherscan_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ETHERSCAN")
inactiveSecret := testSecrets.MustGetField("ETHERSCAN_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a etherscan secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Etherscan,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a etherscan secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Etherscan,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Etherscan.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Etherscan.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/etherscan/etherscan_test.go
================================================
package etherscan
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
api_version: v1
etherscan_secret: "9VROD0TR8VNW4ZEC0U2YK5W9X0B2HO1KAD"
base_url: "https://api.example.com/$api_version/example"
query: "apikey=$etherscan_secret"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "9VROD0TR8VNW4ZEC0U2YK5W9X0B2HO1KAD"
)
func TestEtherScan_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/ethplorer/ethplorer.go
================================================
package ethplorer
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ethplorer"}) + `\b([a-z0-9A-Z-]{22})\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{"ethplorer"}
}
// FromData will find and optionally verify Ethplorer 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Ethplorer,
Raw: []byte(resMatch),
}
if verify {
payload := strings.NewReader("apiKey=" + resMatch + "&addresses=0xb2930b35844a230f00e51431acae96fe543a0347%2C0xb52d3141ee731fac89927476c6a5207b37cd72ff")
req, err := http.NewRequestWithContext(ctx, "POST", "https://api-mon.ethplorer.io/createPool", payload)
if err != nil {
continue
}
req.Header.Add("accept", "application/json")
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Ethplorer
}
func (s Scanner) Description() string {
return "Ethplorer API keys can be used to interact with the Ethplorer service, which provides access to Ethereum blockchain data and analytics."
}
================================================
FILE: pkg/detectors/ethplorer/ethplorer_integration_test.go
================================================
//go:build detectors
// +build detectors
package ethplorer
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestEthplorer_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("ETHPLORER")
inactiveSecret := testSecrets.MustGetField("ETHPLORER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a ethplorer secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Ethplorer,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a ethplorer secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Ethplorer,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Ethplorer.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Ethplorer.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/ethplorer/ethplorer_test.go
================================================
package ethplorer
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Payload"
api_version: v1
ethplorer_secret: "QGp6JMwswjqb5FJFGuslKQ"
base_url: "https://api.example.com/$api_version/example"
query: ""
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "QGp6JMwswjqb5FJFGuslKQ"
)
func TestEthplorer_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/eventbrite/eventbrite.go
================================================
package eventbrite
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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"eventbrite"}) + `\b([0-9A-Z]{20})\b`)
)
func (s *Scanner) getClient() *http.Client {
if s.client == nil {
return defaultClient
}
return s.client
}
// 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{"eventbrite"}
}
// FromData will find and optionally verify Eventbrite 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)
uniqueTokenMatches := make(map[string]struct{})
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueTokenMatches[match[1]] = struct{}{}
}
for token := range uniqueTokenMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Eventbrite,
Raw: []byte(token),
ExtraData: map[string]string{},
}
if verify {
extraData, isVerified, verificationErr := verifyEventBrite(ctx, s.getClient(), token)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
s1.ExtraData = extraData
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Eventbrite
}
func (s Scanner) Description() string {
return "Eventbrite is an event management and ticketing website. Eventbrite API keys can be used to access and manage event data."
}
func verifyEventBrite(ctx context.Context, client *http.Client, token string) (map[string]string, bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.eventbriteapi.com/v3/users/me/?token="+token, nil)
if err != nil {
return nil, false, err
}
resp, err := client.Do(req)
if err != nil {
return nil, false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
var response map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, false, err
}
userName := response["name"].(string)
return map[string]string{"user name": userName}, true, nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil, false, nil
default:
return nil, false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/eventbrite/eventbrite_integration_test.go
================================================
//go:build detectors
// +build detectors
package eventbrite
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/detectorspb"
)
func TestEventbrite_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("EVENTBRITE")
inactiveSecret := testSecrets.MustGetField("EVENTBRITE_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a eventbrite secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Eventbrite,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a eventbrite secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Eventbrite,
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 eventbrite secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Eventbrite,
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 eventbrite secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Eventbrite,
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("Eventbrite.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Eventbrite.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/eventbrite/eventbrite_test.go
================================================
package eventbrite
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
api_version: v1
eventbrite_secret: "1SS1TOXV0S90JCAQ3G8F"
base_url: "https://api.example.com/$api_version/example"
query: "token=$eventbrite_secret"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "1SS1TOXV0S90JCAQ3G8F"
)
func TestEventBrite_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/everhour/everhour.go
================================================
package everhour
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"everhour"}) + `\b([0-9Aa-f]{4}-[0-9a-f]{4}-[0-9a-f]{6}-[0-9a-f]{6}-[0-9a-f]{8})\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{"everhour"}
}
// FromData will find and optionally verify Everhour 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Everhour,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.everhour.com/clients", nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-Api-Key", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Everhour
}
func (s Scanner) Description() string {
return "Everhour is a time tracking software for teams. Everhour API keys can be used to access and manage project and time tracking data."
}
================================================
FILE: pkg/detectors/everhour/everhour_integration_test.go
================================================
//go:build detectors
// +build detectors
package everhour
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestEverhour_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("EVERHOUR")
inactiveSecret := testSecrets.MustGetField("EVERHOUR_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a everhour secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Everhour,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a everhour secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Everhour,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Everhour.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Everhour.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/everhour/everhour_test.go
================================================
package everhour
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
api_version: v1
everhour_secret: "a289-1dad-dbeeeb-2c0b1f-dc0ed546"
base_url: "https://api.example.com/$api_version/example"
query: ""
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "a289-1dad-dbeeeb-2c0b1f-dc0ed546"
)
func TestEventBrite_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/exchangerateapi/exchangerateapi.go
================================================
package exchangerateapi
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"exchangerate", "exchange-rate"}) + `\b([a-f0-9]{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{"exchangerate", "exchange-rate"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ExchangeRateAPI
}
func (s Scanner) Description() string {
return "An API key for determining the exchange rate of currencies"
}
// FromData will find and optionally verify ExchangeRateAPI 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ExchangeRateAPI,
Raw: []byte(resMatch),
}
if verify {
isVerified, verificationErr := verifyExchangeRateKey(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
}
return results, nil
}
func verifyExchangeRateKey(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://v6.exchangerate-api.com/v6/latest/USD", http.NoBody)
if err != nil {
return false, err
}
// authentication docs: https://www.exchangerate-api.com/docs/authentication
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
resp, err := client.Do(req)
if err != nil {
return false, nil
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/exchangerateapi/exchangerateapi_integration_test.go
================================================
//go:build detectors
// +build detectors
package exchangerateapi
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestExchangeRateAPI_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("EXCHANGERATEAPI")
inactiveSecret := testSecrets.MustGetField("EXCHANGERATEAPI_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a exchangerateapi secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ExchangeRateAPI,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a exchangerateapi secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ExchangeRateAPI,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("ExchangeRateAPI.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("ExchangeRateAPI.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/exchangerateapi/exchangerateapi_test.go
================================================
package exchangerateapi
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
func TestExchangeRateAPI_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "Bearer"
in: "Header"
api_version: v1
exchangerate_secret: "a1039cd66170a7bf214199d4"
base_url: "https://api.example.com/$api_version/example"
query: ""
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`,
want: []string{"a1039cd66170a7bf214199d4"},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/exchangeratesapi/exchangeratesapi.go
================================================
package exchangeratesapi
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"exchangerates"}) + `\b([a-z0-9]{32})\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{"exchangerates"}
}
// FromData will find and optionally verify ExchangeRatesAPI 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ExchangeRatesAPI,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.exchangeratesapi.io/v1/latest?access_key=%s", resMatch), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ExchangeRatesAPI
}
func (s Scanner) Description() string {
return "ExchangeRatesAPI provides exchange rate data for various currencies. The API key can be used to access and retrieve this data."
}
================================================
FILE: pkg/detectors/exchangeratesapi/exchangeratesapi_integration_test.go
================================================
//go:build detectors
// +build detectors
package exchangeratesapi
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestExchangeRatesAPI_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("EXCHANGERATESAPI")
inactiveSecret := testSecrets.MustGetField("EXCHANGERATESAPI_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a exchangeratesapi secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ExchangeRatesAPI,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a exchangeratesapi secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ExchangeRatesAPI,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("ExchangeRatesAPI.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("ExchangeRatesAPI.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/exchangeratesapi/exchangeratesapi_test.go
================================================
package exchangeratesapi
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
api_version: v1
exchangerates_secret: "flo7en8mnclsnz50dme89e9vwr3l9jbb"
base_url: "https://api.example.com/$api_version/example"
query: "accesskey=$exchangerates_secret"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "flo7en8mnclsnz50dme89e9vwr3l9jbb"
)
func TestExchangeRatesAPI_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/exportsdk/exportsdk.go
================================================
package exportsdk
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"exportsdk"}) + `\b([0-9a-z]{5,15}_[0-9a-z-]{36})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"exportsdk"}) + `\b([0-9a-z-]{36})\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{"exportsdk"}
}
// FromData will find and optionally verify ExportSDK 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idmatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idmatch := range idmatches {
resIdMatch := strings.TrimSpace(idmatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ExportSDK,
Raw: []byte(resMatch),
}
if verify {
payload := strings.NewReader(`{ "templateId": "` + resIdMatch + `"}`)
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.exportsdk.com/v1/pdf", payload)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-API-KEY", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ExportSDK
}
func (s Scanner) Description() string {
return "ExportSDK is a service used for exporting data and generating PDFs. ExportSDK keys can be used to authenticate API requests and generate documents."
}
================================================
FILE: pkg/detectors/exportsdk/exportsdk_integration_test.go
================================================
//go:build detectors
// +build detectors
package exportsdk
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestExportSDK_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("EXPORTSDK")
id := testSecrets.MustGetField("EXPORTSDK_TEMPLATE")
inactiveSecret := testSecrets.MustGetField("EXPORTSDK_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a exportsdk secret %s within exportsdk %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ExportSDK,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a exportsdk secret %s within exportsdk %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ExportSDK,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("ExportSDK.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("ExportSDK.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/exportsdk/exportsdk_test.go
================================================
package exportsdk
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Header"
api_version: v1
exportsdk_id: "ipwa96igr30chlcfr8xb7gack2xgfd7ov8zk"
exportsdk_secret: "q6l59i_dd8w6gfvh--le8xasayvsufpvt4uh1pzmu07"
base_url: "https://api.example.com/$api_version/example"
query: ""
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "q6l59i_dd8w6gfvh--le8xasayvsufpvt4uh1pzmu07"
)
func TestExportSDK_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/extractorapi/extractorapi.go
================================================
package extractorapi
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"extractorapi"}) + `\b([a-zA-Z-0-9]{40})\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{"extractorapi"}
}
// FromData will find and optionally verify ExtractorAPI 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ExtractorAPI,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://extractorapi.com/api/v1/extractor?apikey="+resMatch+"&url=example.com", nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_ExtractorAPI
}
func (s Scanner) Description() string {
return "ExtractorAPI is a service for extracting data from various sources. ExtractorAPI keys can be used to access and extract data from these sources."
}
================================================
FILE: pkg/detectors/extractorapi/extractorapi_integration_test.go
================================================
//go:build detectors
// +build detectors
package extractorapi
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestExtractorAPI_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("EXTRACTORAPI")
inactiveSecret := testSecrets.MustGetField("EXTRACTORAPI_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a extractorapi secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ExtractorAPI,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a extractorapi secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_ExtractorAPI,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("ExtractorAPI.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("ExtractorAPI.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/extractorapi/extractorapi_test.go
================================================
package extractorapi
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `
# Configuration File: config.yaml
database:
host: $DB_HOST
port: $DB_PORT
username: $DB_USERNAME
password: $DB_PASS # IMPORTANT: Do not share this password publicly
api:
auth_type: "API-Key"
in: "Path"
api_version: v1
extractorapi_secret: "jSCInysVesUIQ8vn7ZIQg3vKUCB8FgMnXTvJ4CKN"
base_url: "https://api.example.com/$api_version/example"
query: "apikey=$extractorapi_secret"
response_code: 200
# Notes:
# - Remember to rotate the secret every 90 days.
# - The above credentials should only be used in a secure environment.
`
secret = "jSCInysVesUIQ8vn7ZIQg3vKUCB8FgMnXTvJ4CKN"
)
func TestExtractorAPI_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/facebookoauth/facebookoauth.go
================================================
package facebookoauth
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
apiIdPat = regexp.MustCompile(detectors.PrefixRegex([]string{"facebook"}) + `\b([0-9]{15,18})\b`) // not actually sure of the upper bound
apiSecretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"facebook"}) + `\b([A-Za-z0-9]{32})\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{"facebook"}
}
// FromData will find and optionally verify FacebookOAuth 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)
apiIdMatches := apiIdPat.FindAllStringSubmatch(dataStr, -1)
apiSecretMatches := apiSecretPat.FindAllStringSubmatch(dataStr, -1)
for _, apiIdMatch := range apiIdMatches {
apiIdRes := strings.TrimSpace(apiIdMatch[1])
for _, apiSecretMatch := range apiSecretMatches {
apiSecretRes := strings.TrimSpace(apiSecretMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FacebookOAuth,
Redacted: apiIdRes,
Raw: []byte(apiSecretRes),
RawV2: []byte(apiIdRes + apiSecretRes),
}
if verify {
// thanks https://stackoverflow.com/questions/15621471/validate-a-facebook-app-id-and-app-secret
// https://stackoverflow.com/questions/24401241/how-to-get-a-facebook-access-token-using-appid-and-app-secret-without-any-login
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://graph.facebook.com/me?access_token=%s|%s", apiIdRes, apiSecretRes), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FacebookOAuth
}
func (s Scanner) Description() string {
return "Facebook OAuth tokens are used to authenticate users and provide access to Facebook's API services."
}
================================================
FILE: pkg/detectors/facebookoauth/facebookoauth_integration_test.go
================================================
//go:build detectors
// +build detectors
package facebookoauth
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFacebookOAuth_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
appId := testSecrets.MustGetField("FACEBOOK_APP_ID")
appSecret := testSecrets.MustGetField("FACEBOOK_APP_SECRET")
inactiveAppSecret := testSecrets.MustGetField("FACEBOOK_APP_SECRET_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a facebook appid %s and secret %s within", appId, appSecret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FacebookOAuth,
Redacted: appId,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a facebook appid %s and secret %s within but not valid", inactiveAppSecret, appId)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FacebookOAuth,
Redacted: appId,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("FacebookOAuth.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("FacebookOAuth.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/facebookoauth/facebookoauth_test.go
================================================
package facebookoauth
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Facebook",
"type": "Detector",
"api": true,
"authentication_type": "OAuth",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"facebook_appid": "5295912532069628",
"facebook_secret": "rw6rTIk14bOEW84MkNbLVqVbrLJugJo7"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "5295912532069628rw6rTIk14bOEW84MkNbLVqVbrLJugJo7"
)
func TestFacebookOAuth_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/faceplusplus/faceplusplus.go
================================================
package faceplusplus
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"faceplusplus"}) + `\b([0-9a-zA-Z_-]{32})\b`)
secretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"faceplusplus"}) + `\b([0-9a-zA-Z_-]{32})\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{"faceplusplus"}
}
// FromData will find and optionally verify Faceplusplus 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
secretMatches := secretPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, secretMatch := range secretMatches {
resSecret := strings.TrimSpace(secretMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FacePlusPlus,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resSecret),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("https://api-us.faceplusplus.com/facepp/v3/faceset/getfacesets?api_key=%s&api_secret=%s", resMatch, resSecret), nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FacePlusPlus
}
func (s Scanner) Description() string {
return "Face++ is a facial recognition service that provides APIs for detecting and analyzing faces. Face++ API keys and secrets can be used to access and manipulate these services."
}
================================================
FILE: pkg/detectors/faceplusplus/faceplusplus_integration_test.go
================================================
//go:build detectors
// +build detectors
package faceplusplus
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFaceplusplus_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
key := testSecrets.MustGetField("FACEPLUSPLUS_KEY")
secret := testSecrets.MustGetField("FACEPLUSPLUS_SECRET")
inactiveSecret := testSecrets.MustGetField("FACEPLUSPLUS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a faceplusplus key %s within faceplusplus secret %s", key, secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FacePlusPlus,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a facepluspluskey %s within faceplusplussecret %s but not valid", key, inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FacePlusPlus,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Faceplusplus.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Faceplusplus.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/faceplusplus/faceplusplus_test.go
================================================
package faceplusplus
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "FacePlusPlus",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"faceplusplus_id": "ipScAHzUxOS2CQ3JwTdIDG1ClxZl_iVH",
"faceplusplus_secret": "Qomsw0IQtp3iz1jlxAqQJO5afpbeEeAh"
},
"expected_response": "200",
"method": "POST",
"deprecated": false
}]`
secrets = []string{
// TODO: Add logic to avoid verification when key and id is same because the regex is same for both
"Qomsw0IQtp3iz1jlxAqQJO5afpbeEeAhQomsw0IQtp3iz1jlxAqQJO5afpbeEeAh",
"ipScAHzUxOS2CQ3JwTdIDG1ClxZl_iVHQomsw0IQtp3iz1jlxAqQJO5afpbeEeAh",
"Qomsw0IQtp3iz1jlxAqQJO5afpbeEeAhipScAHzUxOS2CQ3JwTdIDG1ClxZl_iVH",
"ipScAHzUxOS2CQ3JwTdIDG1ClxZl_iVHipScAHzUxOS2CQ3JwTdIDG1ClxZl_iVH",
}
)
func TestFacePlusPlus_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: secrets,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/falsepositives.go
================================================
package detectors
import (
_ "embed"
"math"
"strings"
"unicode"
"unicode/utf8"
ahocorasick "github.com/BobuSumisu/aho-corasick"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var (
DefaultFalsePositives = map[FalsePositive]struct{}{
"example": {}, "xxxxxx": {}, "aaaaaa": {}, "abcde": {}, "00000": {}, "sample": {}, "*****": {},
}
UuidFalsePositives map[FalsePositive]struct{}
)
type FalsePositive string
type CustomFalsePositiveChecker interface {
// IsFalsePositive returns two values:
// 1. Whether the result is a false positive.
// 2. If #1 is `true`, the reason why.
IsFalsePositive(result Result) (bool, string)
}
var (
filter *ahocorasick.Trie
//go:embed "fp_badlist.txt"
badList []byte
//go:embed "fp_words.txt"
wordList []byte
//go:embed "fp_programmingbooks.txt"
programmingBookWords []byte
//go:embed "fp_uuids.txt"
uuidList []byte
)
func init() {
// Populate trie.
builder := ahocorasick.NewTrieBuilder()
wordList := bytesToCleanWordList(wordList)
builder.AddStrings(wordList)
badList := bytesToCleanWordList(badList)
builder.AddStrings(badList)
programmingBookWords := bytesToCleanWordList(programmingBookWords)
builder.AddStrings(programmingBookWords)
uuidList := bytesToCleanWordList(uuidList)
builder.AddStrings(uuidList)
filter = builder.Build()
// Populate custom FalsePositive list
UuidFalsePositives = make(map[FalsePositive]struct{}, len(uuidList))
for _, uuid := range uuidList {
UuidFalsePositives[FalsePositive(uuid)] = struct{}{}
}
}
func GetFalsePositiveCheck(detector Detector) func(Result) (bool, string) {
checker, ok := detector.(CustomFalsePositiveChecker)
if ok {
return checker.IsFalsePositive
}
return func(res Result) (bool, string) {
return IsKnownFalsePositive(string(res.Raw), DefaultFalsePositives, true)
}
}
// IsKnownFalsePositive returns whether a finding is (likely) a known false positive, and the reason for the detection.
//
// Currently, this includes: english word in key or matches common example patterns.
// Only the secret key material should be passed into this function
func IsKnownFalsePositive(match string, falsePositives map[FalsePositive]struct{}, wordCheck bool) (bool, string) {
if !utf8.ValidString(match) {
return true, "invalid utf8"
}
lower := strings.ToLower(match)
if _, exists := falsePositives[FalsePositive(lower)]; exists {
return true, "matches term: " + lower
}
for fp := range falsePositives {
fps := string(fp)
if strings.Contains(lower, fps) {
return true, "contains term: " + fps
}
}
if wordCheck {
if m := filter.MatchFirstString(lower); m != nil {
return true, "matches wordlist: " + m.MatchString()
}
}
return false, ""
}
func HasDigit(key string) bool {
for _, ch := range key {
if unicode.IsDigit(ch) {
return true
}
}
return false
}
func bytesToCleanWordList(data []byte) []string {
words := make(map[string]struct{})
for _, word := range strings.Split(string(data), "\n") {
if strings.TrimSpace(word) != "" {
words[strings.TrimSpace(strings.ToLower(word))] = struct{}{}
}
}
wordList := make([]string, 0, len(words))
for word := range words {
wordList = append(wordList, word)
}
return wordList
}
func StringShannonEntropy(input string) float64 {
chars := make(map[rune]float64)
inverseTotal := 1 / float64(len(input)) // precompute the inverse
for _, char := range input {
chars[char]++
}
entropy := 0.0
for _, count := range chars {
probability := count * inverseTotal
entropy += probability * math.Log2(probability)
}
return -entropy
}
// FilterResultsWithEntropy filters out determinately unverified results that have a shannon entropy below the given value.
func FilterResultsWithEntropy(ctx context.Context, results []Result, entropy float64, shouldLog bool) []Result {
var filteredResults []Result
for _, result := range results {
if !result.Verified {
if result.Raw != nil {
if StringShannonEntropy(string(result.Raw)) >= entropy {
filteredResults = append(filteredResults, result)
} else {
if shouldLog {
ctx.Logger().Info("Filtered out result with low entropy", "result", result)
}
}
} else {
filteredResults = append(filteredResults, result)
}
} else {
filteredResults = append(filteredResults, result)
}
}
return filteredResults
}
================================================
FILE: pkg/detectors/falsepositives_test.go
================================================
package detectors
import (
"context"
_ "embed"
"testing"
"github.com/stretchr/testify/assert"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type fakeDetector struct{}
type customFalsePositiveChecker struct{ fakeDetector }
func (d fakeDetector) FromData(ctx context.Context, verify bool, data []byte) ([]Result, error) {
return nil, nil
}
func (d fakeDetector) Keywords() []string {
return nil
}
func (d fakeDetector) Type() detectorspb.DetectorType {
return detectorspb.DetectorType(0)
}
func (f fakeDetector) Description() string { return "" }
func (d customFalsePositiveChecker) IsFalsePositive(result Result) (bool, string) {
return IsKnownFalsePositive(string(result.Raw), map[FalsePositive]struct{}{"a specific magic string": {}}, false)
}
// This test validates that GetFalsePositiveCheck, when invoked on a detector that does not implement
// CustomFalsePositiveChecker, returns a predicate that behaves as expected.
func TestGetFalsePositiveCheck_DefaultLogic(t *testing.T) {
testCases := []struct {
raw string
isFalsePositive bool
}{
{"00000", true}, // "default" false positive list
{"number", true}, // from wordlist
{"00000000-0000-0000-0000-000000000000", true}, // from uuid list
{"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", true}, // from uuid list
{"hga8adshla3434g", false},
{"f795f7db-2dfe-4095-96f3-8f8370c735f9", false},
}
for _, tt := range testCases {
isFalsePositive, _ := GetFalsePositiveCheck(fakeDetector{})(Result{Raw: []byte(tt.raw)})
assert.Equal(t, tt.isFalsePositive, isFalsePositive, "secret %q had unexpected false positive status", tt.raw)
}
}
// This test validates that GetFalsePositiveCheck, when invoked on a detector that implements
// CustomFalsePositiveChecker, returns a predicate that behaves as expected. (Specifically, the predicate should not
// flag secrets that are present in the standard false positive lists.)
func TestGetFalsePositiveCheck_CustomLogic(t *testing.T) {
testCases := []struct {
raw string
isFalsePositive bool
}{
{"a specific magic string", true}, // the specific value the custom checker is looking for
{"00000", false},
{"number", false},
{"00000000-0000-0000-0000-000000000000", false},
{"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", false},
{"hga8adshla3434g", false},
{"f795f7db-2dfe-4095-96f3-8f8370c735f9", false},
}
for _, tt := range testCases {
isFalsePositive, _ := GetFalsePositiveCheck(customFalsePositiveChecker{})(Result{Raw: []byte(tt.raw)})
assert.Equal(t, tt.isFalsePositive, isFalsePositive, "secret %q had unexpected false positive status", tt.raw)
}
}
func TestIsFalsePositive(t *testing.T) {
type args struct {
match string
falsePositives map[FalsePositive]struct{}
useWordlist bool
}
tests := []struct {
name string
args args
want bool
}{
{
name: "fp",
args: args{
match: "example",
falsePositives: DefaultFalsePositives,
useWordlist: false,
},
want: true,
},
{
name: "fp - in wordlist",
args: args{
match: "sdfdsfprivatesfsdfd",
falsePositives: DefaultFalsePositives,
useWordlist: true,
},
want: true,
},
{
name: "fp - not in wordlist",
args: args{
match: "sdfdsfsfsdfd",
falsePositives: DefaultFalsePositives,
useWordlist: true,
},
want: false,
},
{
name: "not fp",
args: args{
match: "notafp123",
falsePositives: DefaultFalsePositives,
useWordlist: false,
},
want: false,
},
{
name: "fp - in wordlist exact match",
args: args{
match: "private",
falsePositives: DefaultFalsePositives,
useWordlist: true,
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got, _ := IsKnownFalsePositive(tt.args.match, tt.args.falsePositives, tt.args.useWordlist); got != tt.want {
t.Errorf("IsKnownFalsePositive() = %v, want %v", got, tt.want)
}
})
}
}
func TestStringShannonEntropy(t *testing.T) {
type args struct {
input string
}
tests := []struct {
name string
args args
want float64
}{
{
name: "entropy 1",
args: args{
input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
want: 0,
},
{
name: "entropy 2",
args: args{
input: "aaaaaaaaaaaaaaaaaaaaaaaaaaab",
},
want: 0.22,
},
{
name: "entropy 3",
args: args{
input: "aaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaab",
},
want: 0.22,
},
{
name: "empty",
args: args{
input: "",
},
want: 0.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := StringShannonEntropy(tt.args.input)
if len(tt.args.input) > 0 && tt.want != 0 {
assert.InEpsilon(t, tt.want, got, 0.1)
} else {
assert.Equal(t, tt.want, got)
}
})
}
}
func BenchmarkDefaultIsKnownFalsePositive(b *testing.B) {
for i := 0; i < b.N; i++ {
// Use a string that won't be found in any dictionary for the worst case check.
IsKnownFalsePositive("aoeuaoeuaoeuaoeuaoeuaoeu", DefaultFalsePositives, true)
}
}
================================================
FILE: pkg/detectors/fastforex/fastforex.go
================================================
package fastforex
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"fastforex"}) + `\b([a-z0-9-]{28})\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{"fastforex"}
}
// FromData will find and optionally verify FastForex 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FastForex,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.fastforex.io/fetch-all?api_key=%s", resMatch), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FastForex
}
func (s Scanner) Description() string {
return "FastForex provides foreign exchange rate data. FastForex API keys can be used to access and retrieve this data."
}
================================================
FILE: pkg/detectors/fastforex/fastforex_integration_test.go
================================================
//go:build detectors
// +build detectors
package fastforex
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFastForex_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FASTFOREX")
inactiveSecret := testSecrets.MustGetField("FASTFOREX_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a fastforex secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FastForex,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a fastforex secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FastForex,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("FastForex.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("FastForex.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/fastforex/fastforex_test.go
================================================
package fastforex
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "FastForex",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"fastforex_secret": "jk-qatdz1xcgoz3yssqexstefbtq"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "jk-qatdz1xcgoz3yssqexstefbtq"
)
func TestFastForex_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/fastlypersonaltoken/fastlypersonaltoken.go
================================================
package fastlypersonaltoken
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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"fastly"}) + `\b([A-Za-z0-9_-]{32})\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{"fastly"}
}
type token struct {
TokenID string `json:"id"`
UserID string `json:"user_id"`
ExpiresAt string `json:"expires_at"`
Scope string `json:"scope"`
}
// FromData will find and optionally verify FastlyPersonalToken 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)
var uniqueMatches = make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueMatches[matches[1]] = struct{}{}
}
for match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FastlyPersonalToken,
Raw: []byte(match),
}
if verify {
extraData, verified, verificationErr := verifyFastlyApiToken(ctx, match)
s1.Verified = verified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr, match)
if s1.Verified {
s1.AnalysisInfo = map[string]string{
"key": match,
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FastlyPersonalToken
}
func (s Scanner) Description() string {
return "Fastly is a content delivery network (CDN) and cloud service provider. Fastly personal tokens can be used to authenticate API requests to Fastly services."
}
func verifyFastlyApiToken(ctx context.Context, apiToken string) (map[string]string, bool, error) {
// api-docs: https://www.fastly.com/documentation/reference/api/auth-tokens/user/
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.fastly.com/tokens/self", nil)
if err != nil {
return nil, false, err
}
// add api key in the header
req.Header.Add("Fastly-Key", apiToken)
resp, err := client.Do(req)
if err != nil {
return nil, false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
var self token
if err = json.NewDecoder(resp.Body).Decode(&self); err != nil {
return nil, false, err
}
// capture token details in the map
extraData := map[string]string{
// token id is the alphanumeric string uniquely identifying a token
"token_id": self.TokenID,
// user id is the alphanumeric string uniquely identifying the user
"user_id": self.UserID,
// expires at is time-stamp (UTC) of when the token will expire
"token_expires_at": self.ExpiresAt,
// token scope is space-delimited list of authorization scope of the token
"token_scope": self.Scope,
}
// if expires at is empty which mean token is set to never expire, add 'Never' as the value
if extraData["token_expires_at"] == "" {
extraData["token_expires_at"] = "never"
}
return extraData, true, nil
case http.StatusUnauthorized, http.StatusForbidden:
// as per fastly documentation: An HTTP 401 response is returned on an expired token. An HTTP 403 response is returned on an invalid access token.
return nil, false, nil
default:
return nil, false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
================================================
FILE: pkg/detectors/fastlypersonaltoken/fastlypersonaltoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package fastlypersonaltoken
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/detectorspb"
)
func TestFastlyPersonalToken_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FASTLYPERSONALTOKEN_TOKEN")
inactiveSecret := testSecrets.MustGetField("FASTLYPERSONALTOKEN_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: Scanner{},
args: args{
ctx: ctx,
data: []byte(fmt.Sprintf("You can find a fastlypersonaltoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FastlyPersonalToken,
Verified: true,
ExtraData: map[string]string{
"token_id": "2ICO7ArmhY8OMiiOyNpXfc",
"user_id": "7anDA1ct17E8pkFAE0tJkk",
"token_expires_at": "never",
"token_scope": "global:read global",
},
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a fastlypersonaltoken secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FastlyPersonalToken,
Verified: false,
ExtraData: nil,
},
},
wantErr: 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,
},
}
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("FastlyPersonalToken.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])
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "AnalysisInfo")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("FastlyPersonalToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/fastlypersonaltoken/fastlypersonaltoken_test.go
================================================
package fastlypersonaltoken
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
// example picked from: https://github.com/ryan-miller/learn-go-with-tests/blob/181467c9e512f7e68d3f3cbcea89f0050982416c/fastly/users.go#L22
validPattern = `
// headers and header values
const fastlyKeyToken string = "Fastly-Key"
const fastlyKey string = "TVAWji0p7uDI6OP9DyWvmV-vgoUoXIuf"
const contentTypeToken string = "Content-Type"
const appJsonContentType = "application/json"`
validPatternToken = "TVAWji0p7uDI6OP9DyWvmV-vgoUoXIuf"
invalidPattern = `
// headers and header values
const fastlyKeyToken string = "Fastly-Key"
const fastlyKey string = "$FASTLY_KEY"
const contentTypeToken string = "Content-Type"
const appJsonContentType = "application/json"`
)
func TestFastlyPersonalToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{validPatternToken},
},
{
name: "invalid pattern",
input: invalidPattern,
want: nil,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/feedier/feedier.go
================================================
package feedier
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"feedier"}) + `\b([a-z0-9A-Z]{32})\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{"feedier"}
}
// FromData will find and optionally verify Feedier 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Feedier,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.feedier.com/v1/carriers", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Feedier
}
func (s Scanner) Description() string {
return "Feedier is a feedback management platform that allows businesses to collect and analyze customer feedback. Feedier API keys can be used to access and manage feedback data."
}
================================================
FILE: pkg/detectors/feedier/feedier_integration_test.go
================================================
//go:build detectors
// +build detectors
package feedier
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFeedier_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FEEDIER_TOKEN")
inactiveSecret := testSecrets.MustGetField("FEEDIER_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a feedier secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Feedier,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a feedier secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Feedier,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Feedier.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Feedier.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/feedier/feedier_test.go
================================================
package feedier
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Feedier",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"feedier_secret": "kZ581ej1fDjtvE8iXNcgFJ8V2t0Lfv1d"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "kZ581ej1fDjtvE8iXNcgFJ8V2t0Lfv1d"
)
func TestFeedier_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/fetchrss/fetchrss.go
================================================
package fetchrss
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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"fetchrss"}) + `\b([a-zA-Z0-9.]{40})\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{"fetchrss"}
}
// FromData will find and optionally verify Fetchrss 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: detectorspb.DetectorType_Fetchrss,
Raw: []byte(token),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
verified, verificationErr := verifyToken(ctx, client, token)
s1.Verified = verified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
return results, nil
}
func verifyToken(ctx context.Context, client *http.Client, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://fetchrss.com/api/v1/feed/list?auth="+token, nil)
if err != nil {
return false, err
}
res, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
// The API seems to always return a 200 status code.
// See: https://fetchrss.com/developers
if res.StatusCode != http.StatusOK {
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
var apiRes response
if err := json.NewDecoder(res.Body).Decode(&apiRes); err != nil {
return false, err
}
if apiRes.Success {
// The key is valid.
return true, nil
} else if apiRes.Error.Code == 401 {
// The key is invalid.
return false, nil
} else {
return false, fmt.Errorf("unexpected error: [code=%d, message=%s]", apiRes.Error.Code, apiRes.Error.Message)
}
}
type response struct {
Success bool `json:"success"`
Error struct {
Message string `json:"message"`
Code int `json:"code"`
} `json:"error"`
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Fetchrss
}
func (s Scanner) Description() string {
return "FetchRSS is a service used to convert web content into RSS feeds. FetchRSS API keys can be used to manage and access these feeds."
}
================================================
FILE: pkg/detectors/fetchrss/fetchrss_integration_test.go
================================================
//go:build detectors
// +build detectors
package fetchrss
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFetchrss_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FETCHRSS_TOKEN")
inactiveSecret := testSecrets.MustGetField("FETCHRSS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a fetchrss secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Fetchrss,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a fetchrss secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Fetchrss,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Fetchrss.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Fetchrss.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/fetchrss/fetchrss_test.go
================================================
package fetchrss
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "FetchRSS",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"fetchrss_secret": "x3lljmW2KHoljMrcFSTN5nWWAvDjwdQA0ed0QmHL"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "x3lljmW2KHoljMrcFSTN5nWWAvDjwdQA0ed0QmHL"
)
func TestFetchRSS_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/fibery/fibery.go
================================================
package fibery
import (
"context"
"fmt"
"io"
"net/http"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"fibery"}) + `\b([0-9a-f]{8}\.[0-9a-f]{35})\b`)
domainPat = regexp.MustCompile(`(?:https?:\/\/)?([a-zA-Z0-9-]{1,63})\.fibery\.io(?:\/.*)?`)
)
// 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{".fibery.io"}
}
// Description returns a description for the result being detected
func (s Scanner) Description() string {
return "Fibery is a work management platform that combines various tools for project management, knowledge management, and software development. Fibery API tokens can be used to access and modify data within a Fibery workspace."
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData will find and optionally verify Fibery 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)
uniqueSecrets := make(map[string]struct{})
uniqueDomains := make(map[string]struct{})
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueSecrets[match[1]] = struct{}{}
}
for _, match := range domainPat.FindAllStringSubmatch(dataStr, -1) {
uniqueDomains[match[1]] = struct{}{}
}
for secret := range uniqueSecrets {
for domain := range uniqueDomains {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Fibery,
Raw: []byte(secret),
}
if verify {
isVerified, verificationErr := verifyMatch(ctx, s.getClient(), secret, domain)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, secret, domain)
}
results = append(results, s1)
}
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, secret, domain string) (bool, error) {
timeout := 10 * time.Second
client.Timeout = timeout
url := fmt.Sprintf("https://%s.fibery.io/api/commands", domain)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Token %s", secret))
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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Fibery
}
================================================
FILE: pkg/detectors/fibery/fibery_integration_test.go
================================================
//go:build detectors
// +build detectors
package fibery
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFibery_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FIBERY_SECRET")
domain := testSecrets.MustGetField("FIBERY_DOMAIN")
inactiveSecret := testSecrets.MustGetField("FIBERY_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a fibery secret %s within fibery domain %s ", secret, domain)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Fibery,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a fibery secret %s within fibery domain %s but not valid", inactiveSecret, domain)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Fibery,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Fibery.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Fibery.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/fibery/fibery_test.go
================================================
package fibery
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Fibery",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://detector.fibery.io/example",
"domain": "nonprod",
"test_secrets": {
"fibery_secret": "42b2eda8.3fe6b086bb21be7e3548368626d01aaf2cd"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "42b2eda8.3fe6b086bb21be7e3548368626d01aaf2cd"
)
func TestFibery_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/figmapersonalaccesstoken/v1/figmapersonalaccesstoken.go
================================================
package figmapersonalaccesstoken
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)
func (Scanner) Version() int { return 1 }
var (
defaultClient = common.SaneHttpClient()
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"figma"}) + `\b([0-9]{6}-[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})\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{"figma"}
}
// FromData will find and optionally verify FigmaPersonalAccessToken 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FigmaPersonalAccessToken,
Raw: []byte(resMatch),
ExtraData: map[string]string{
"version": fmt.Sprintf("%d", s.Version()),
},
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.figma.com/v1/me", nil)
if err != nil {
continue
}
req.Header.Add("X-Figma-Token", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
} else if res.StatusCode != 403 {
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
s1.SetVerificationError(err, resMatch)
}
} else {
s1.SetVerificationError(err, resMatch)
}
if s1.Verified {
s1.AnalysisInfo = map[string]string{"token": resMatch}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FigmaPersonalAccessToken
}
func (s Scanner) Description() string {
return "Figma is a web-based design tool. Personal Access Tokens can be used to access and modify design files and other resources."
}
================================================
FILE: pkg/detectors/figmapersonalaccesstoken/v1/figmapersonalaccesstoken_test.go
================================================
package figmapersonalaccesstoken
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Figma",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"figma_secret": "647501-6p71dd66-3k6s-un9a-0ri0-0ypi87cz3rmx"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "647501-6p71dd66-3k6s-un9a-0ri0-0ypi87cz3rmx"
)
func TestFigmaPersonalAccessToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/figmapersonalaccesstoken/v1/figmapersonalacesstoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package figmapersonalaccesstoken
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/detectorspb"
)
func TestFigmaPersonalAccessToken_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FIGMAPERSONALACCESSTOKEN_TOKEN")
inactiveSecret := testSecrets.MustGetField("FIGMAPERSONALACCESSTOKEN_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a figmapersonalaccesstoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FigmaPersonalAccessToken,
Verified: true,
ExtraData: map[string]string{
"version": "1",
},
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a figmapersonalaccesstoken secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FigmaPersonalAccessToken,
Verified: false,
ExtraData: map[string]string{
"version": "1",
},
},
},
wantErr: false,
},
{
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 figmapersonalaccesstoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FigmaPersonalAccessToken,
Verified: false,
ExtraData: map[string]string{
"version": "1",
},
},
},
wantErr: false,
wantVerificationErr: true,
},
{
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 figmapersonalaccesstoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FigmaPersonalAccessToken,
Verified: false,
ExtraData: map[string]string{
"version": "1",
},
},
},
wantErr: false,
wantVerificationErr: true,
},
{
name: "not found",
s: Scanner{},
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("FigmaPersonalAccessToken.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("FigmaPersonalAccessToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/figmapersonalaccesstoken/v2/figmapersonalaccesstoken_integration_test.go
================================================
//go:build detectors
// +build detectors
package figmapersonalaccesstoken
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/detectorspb"
)
func TestFigmaPersonalAccessToken_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("FIGMAPERSONALACCESSTOKEN_V2_TOKEN")
inactiveSecret := testSecrets.MustGetField("FIGMAPERSONALACCESSTOKEN_V2_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a figmapersonalaccesstoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FigmaPersonalAccessToken,
Verified: true,
ExtraData: map[string]string{
"version": "2",
},
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a figmapersonalaccesstoken secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FigmaPersonalAccessToken,
Verified: false,
ExtraData: map[string]string{
"version": "2",
},
},
},
wantErr: false,
},
{
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 figmapersonalaccesstoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FigmaPersonalAccessToken,
Verified: false,
ExtraData: map[string]string{
"version": "2",
},
},
},
wantErr: false,
wantVerificationErr: true,
},
{
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 figmapersonalaccesstoken secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FigmaPersonalAccessToken,
Verified: false,
ExtraData: map[string]string{
"version": "2",
},
},
},
wantErr: false,
wantVerificationErr: true,
},
{
name: "not found",
s: Scanner{},
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("FigmaPersonalAccessToken.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("FigmaPersonalAccessToken.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/figmapersonalaccesstoken/v2/figmapersonalaccesstoken_v2.go
================================================
package figmapersonalaccesstoken
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)
func (Scanner) Version() int { return 2 }
var (
defaultClient = common.SaneHttpClient()
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"figma"}) + `\b(fig[d|((u|o)(r|h)?)]_[a-z0-9A-Z_-]{40})\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{"figma"}
}
// Description returns a description for the result being detected.
func (s Scanner) Description() string {
return "Figma is a collaborative interface design tool. Figma Personal Access Tokens can be used to access and manipulate design files and other resources on behalf of a user."
}
// FromData will find and optionally verify FigmaPersonalAccessToken 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FigmaPersonalAccessToken,
Raw: []byte(resMatch),
ExtraData: map[string]string{
"version": fmt.Sprintf("%d", s.Version()),
},
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.figma.com/v1/me", nil)
if err != nil {
continue
}
req.Header.Add("X-Figma-Token", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
} else if res.StatusCode != 403 {
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
s1.SetVerificationError(err, resMatch)
}
} else {
s1.SetVerificationError(err, resMatch)
}
if s1.Verified {
s1.AnalysisInfo = map[string]string{"token": resMatch}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FigmaPersonalAccessToken
}
================================================
FILE: pkg/detectors/figmapersonalaccesstoken/v2/figmapersonalaccesstoken_v2_test.go
================================================
package figmapersonalaccesstoken
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Figma",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"figma_secret": "figr_EZe7plhYvN92IyiDCjkvTcbNVZsuRVpDcHOwNNP1"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "figr_EZe7plhYvN92IyiDCjkvTcbNVZsuRVpDcHOwNNP1"
)
func TestFigmaPersonalAccessToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/fileio/fileio.go
================================================
package fileio
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"fileio"}) + `\b([A-Z0-9.-]{39})\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{"fileio"}
}
// FromData will find and optionally verify FileIO 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FileIO,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://file.io/", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
bodyBytes, err := io.ReadAll(res.Body)
if err == nil {
isJson := json.Valid(bodyBytes)
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
if isJson {
s1.Verified = true
} else {
s1.Verified = false
}
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FileIO
}
func (s Scanner) Description() string {
return "FileIO is a service for temporary file sharing. The detected key can be used to access and manage shared files."
}
================================================
FILE: pkg/detectors/fileio/fileio_integration_test.go
================================================
//go:build detectors
// +build detectors
package fileio
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFileIO_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FILEIO")
inactiveSecret := testSecrets.MustGetField("FILEIO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a fileio secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FileIO,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a fileio secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FileIO,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("FileIO.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("FileIO.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/fileio/fileio_test.go
================================================
package fileio
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Fileio",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"fileio_secret": "4N4VTAX5KCE0L6R56HS9778HVC2.KH83JBNN7F3"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "4N4VTAX5KCE0L6R56HS9778HVC2.KH83JBNN7F3"
)
func TestFileIO_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/finage/finage.go
================================================
package finage
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(API_KEY[0-9A-Z]{32})\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{"finage"}
}
// FromData will find and optionally verify Finage 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Finage,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.finage.co.uk/symbol-list/crypto?apikey=%s", resMatch), nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Finage
}
func (s Scanner) Description() string {
return "Finage provides financial data APIs for stocks, forex, and cryptocurrencies. Finage API keys can be used to access and retrieve financial data."
}
================================================
FILE: pkg/detectors/finage/finage_integration_test.go
================================================
//go:build detectors
// +build detectors
package finage
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFinage_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FINAGE")
inactiveSecret := testSecrets.MustGetField("FINAGE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a finage secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Finage,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a finage secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Finage,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Finage.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Finage.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/finage/finage_test.go
================================================
package finage
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Finage",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"secret": "API_KEYN2B1NFN5CP6CK5BJHY8B15YF535TP681"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "API_KEYN2B1NFN5CP6CK5BJHY8B15YF535TP681"
)
func TestFinage_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/financialmodelingprep/financialmodelingprep.go
================================================
package financialmodelingprep
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"financialmodelingprep"}) + `\b([a-zA-Z0-9]{32})\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{"financialmodelingprep"}
}
// FromData will find and optionally verify FinancialModelingPrep 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FinancialModelingPrep,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://financialmodelingprep.com/api/v3/financial-statement-symbol-lists?apikey=%s", resMatch), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
bodyBytes, err := io.ReadAll(res.Body)
bodyString := string(bodyBytes)
if err == nil {
// valid response should be an array of currencies
// error response is in json
validResponse := strings.Contains(bodyString, `[ "`)
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
if validResponse {
s1.Verified = true
} else {
s1.Verified = false
}
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FinancialModelingPrep
}
func (s Scanner) Description() string {
return "FinancialModelingPrep provides financial data APIs. The API keys can be used to access financial data and related services."
}
================================================
FILE: pkg/detectors/financialmodelingprep/financialmodelingprep_integration_test.go
================================================
//go:build detectors
// +build detectors
package financialmodelingprep
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFinancialModelingPrep_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FINANCIALMODELINGPREP")
inactiveSecret := testSecrets.MustGetField("FINANCIALMODELINGPREP_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a financialmodelingprep secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FinancialModelingPrep,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a financialmodelingprep secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FinancialModelingPrep,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("FinancialModelingPrep.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("FinancialModelingPrep.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/financialmodelingprep/financialmodelingprep_test.go
================================================
package financialmodelingprep
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Financial Modeling Prep",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"financialmodelingprep_secret": "WXEUwkx44VjTRlunqyncJOCDeszMoC6p"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "WXEUwkx44VjTRlunqyncJOCDeszMoC6p"
)
func TestFinancialModelingPrep_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/findl/findl.go
================================================
package findl
import (
"context"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"findl"}) + `\b([a-z0-9]{8}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{12})\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{"findl"}
}
// FromData will find and optionally verify Findl 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Findl,
Raw: []byte(resMatch),
}
if verify {
timeout := 5 * time.Second
client.Timeout = timeout
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.findl.com/v1.0/query?limit=6", nil)
if err != nil {
continue
}
req.Header.Add("X-API-Key", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Findl
}
func (s Scanner) Description() string {
return "Findl is a service used for searching and querying data. Findl API keys can be used to access and modify this data."
}
================================================
FILE: pkg/detectors/findl/findl_integration_test.go
================================================
//go:build detectors
// +build detectors
package findl
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFindl_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FINDL")
inactiveSecret := testSecrets.MustGetField("FINDL_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a findl secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Findl,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a findl secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Findl,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Findl.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Findl.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/findl/findl_test.go
================================================
package findl
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Findl",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"findl_secret": "l06ebuli-0k4m-b5yg-xieh-81s5b9s04ssu"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "l06ebuli-0k4m-b5yg-xieh-81s5b9s04ssu"
)
func TestFindl_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/finnhub/finnhub.go
================================================
package finnhub
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"finnhub"}) + `\b([0-9a-z]{20})\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{"finnhub"}
}
// FromData will find and optionally verify Finnhub 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Finnhub,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://finnhub.io/api/v1/calendar/economic?token=%s", resMatch), nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Finnhub
}
func (s Scanner) Description() string {
return "Finnhub is a financial data provider offering APIs to access market data. Finnhub API keys can be used to retrieve economic calendars, stock prices, and other financial information."
}
================================================
FILE: pkg/detectors/finnhub/finnhub_integration_test.go
================================================
//go:build detectors
// +build detectors
package finnhub
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFinnhub_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FINNHUB")
inactiveSecret := testSecrets.MustGetField("FINNHUB_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a finnhub secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Finnhub,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a finnhub secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Finnhub,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Finnhub.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Finnhub.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/finnhub/finnhub_test.go
================================================
package finnhub
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Finnhub",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"finnhub_secret": "5rjqnul3u250d36i73lc"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "5rjqnul3u250d36i73lc"
)
func TestFinnHub_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/fixerio/fixerio.go
================================================
package fixerio
import (
"context"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"fixer"}) + `\b([A-Za-z0-9]{32})\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{"fixer"}
}
// FromData will find and optionally verify FixerIO 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FixerIO,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://data.fixer.io/api/latest?access_key="+resMatch, nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
continue
}
body := string(bodyBytes)
// if client_id and client_secret is valid -> 403 {"error":"invalid_grant","error_description":"Invalid authorization code"}
// if invalid -> 401 {"error":"access_denied","error_description":"Unauthorized"}
// ingenious!
validResponse := strings.Contains(body, `"success": true`) || strings.Contains(body, `"info":"Access Restricted - Your current Subscription Plan does not support HTTPS Encryption."`)
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 && validResponse {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FixerIO
}
func (s Scanner) Description() string {
return "Fixer.io is a foreign exchange rates and currency conversion API. Fixer.io API keys can be used to access and retrieve current and historical foreign exchange rates."
}
================================================
FILE: pkg/detectors/fixerio/fixerio_integration_test.go
================================================
//go:build detectors
// +build detectors
package fixerio
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFixerIO_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FIXERIO_TOKEN")
inactiveSecret := testSecrets.MustGetField("FIXERIO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a fixerio secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FixerIO,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a fixerio secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FixerIO,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("FixerIO.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("FixerIO.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/fixerio/fixerio_test.go
================================================
package fixerio
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Fixerio",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"fixerio_secret": "adAM8pezol6tzRrFufnOmUSd4UUO2DoZ"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "adAM8pezol6tzRrFufnOmUSd4UUO2DoZ"
)
func TestFixerio_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/flatio/flatio.go
================================================
package flatio
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"flat"}) + `\b([0-9a-z]{128})\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{"flat"}
}
// FromData will find and optionally verify FlatIO 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FlatIO,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.flat.io/v2/me", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FlatIO
}
func (s Scanner) Description() string {
return "FlatIO is a music notation software. FlatIO keys can be used to access and modify musical scores and related data."
}
================================================
FILE: pkg/detectors/flatio/flatio_integration_test.go
================================================
//go:build detectors
// +build detectors
package flatio
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFlatIO_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FLATIO")
inactiveSecret := testSecrets.MustGetField("FLATIO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flatio secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FlatIO,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flatio secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FlatIO,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("FlatIO.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("FlatIO.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/flatio/flatio_test.go
================================================
package flatio
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Flatio",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"flatio_secret": "n8dihgssrd0h0vv51l29da4wneg6ypo7qegcem2k3jcs9f6ywisvqu8vdimwp0m7pzo6ohnb01d13trnpun3couzbhvtlkbu2fsy8tliiww9ggis53s7xi9mvejj2idy"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "n8dihgssrd0h0vv51l29da4wneg6ypo7qegcem2k3jcs9f6ywisvqu8vdimwp0m7pzo6ohnb01d13trnpun3couzbhvtlkbu2fsy8tliiww9ggis53s7xi9mvejj2idy"
)
func TestFlatIO_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/fleetbase/fleetbase.go
================================================
package fleetbase
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(flb_live_[0-9a-zA-Z]{20})\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{"fleetbase"}
}
// FromData will find and optionally verify Fleetbase 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Fleetbase,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.fleetbase.io/v1/contacts/", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Fleetbase
}
func (s Scanner) Description() string {
return "Fleetbase is a platform for building logistics and supply chain applications. Fleetbase API keys can be used to access and manage logistics data and operations."
}
================================================
FILE: pkg/detectors/fleetbase/fleetbase_integration_test.go
================================================
//go:build detectors
// +build detectors
package fleetbase
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFleetbase_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FLEETBASE")
inactiveSecret := testSecrets.MustGetField("FLEETBASE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a fleetbase secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Fleetbase,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a fleetbase secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Fleetbase,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Fleetbase.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Fleetbase.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/fleetbase/fleetbase_test.go
================================================
package fleetbase
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Fleetbase",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"secret": "flb_live_ZtWtb6hVkUMVdUDg2lgK"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "flb_live_ZtWtb6hVkUMVdUDg2lgK"
)
func TestFleetBase_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/flexport/flexport.go
================================================
package flexport
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(shltm_[0-9a-zA-Z-_]{40})`)
)
// 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{"shltm_"}
}
// FromData will find and optionally verify Flexport 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 match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Flexport,
Raw: []byte(match),
ExtraData: map[string]string{
"rotation_guide": "https://howtorotate.com/docs/tutorials/flexport/",
},
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyMatch(ctx, client, match)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, match)
}
results = append(results, s1)
}
return
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) {
// docs: https://docs.logistics-api.flexport.com/2024-04/tag/Webhooks#operation/GetWebhook
url := "https://logistics-api.flexport.com/logistics/api/2024-04/webhooks"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, 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, http.StatusForbidden:
// If the endpoint returns useful information, we can return it as a map.
return true, nil
case http.StatusUnauthorized:
// The secret is determinately not verified (nothing to do)
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Flexport
}
func (s Scanner) Description() string {
return "Flexport is a global logistics company that provides shipping, freight forwarding, and supply chain management services."
}
================================================
FILE: pkg/detectors/flexport/flexport_test.go
================================================
//go:build detectors
// +build detectors
package flexport
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/engine/ahocorasick"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFlexport_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "typical pattern",
input: "flexport_token = 'shltm_ZnpDDh4AEj_n2WHHqjYErtv3ZGS0kH1bWVdl7V9D'",
want: []string{"shltm_ZnpDDh4AEj_n2WHHqjYErtv3ZGS0kH1bWVdl7V9D"},
},
}
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)
}
})
}
}
func TestFlexport_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("FLEXPORT")
inactiveSecret := testSecrets.MustGetField("FLEXPORT_INACTIVE")
secretNoPermissions := testSecrets.MustGetField("FLEXPORT_NO_PERMISSIONS")
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 - with permissions",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flexport secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Flexport,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, verified - without permissions",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flexport secret %s within", secretNoPermissions)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Flexport,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flexport secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Flexport,
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 flexport secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Flexport,
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 flexport secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Flexport,
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("Flexport.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Flexport.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/flickr/flickr.go
================================================
package flickr
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"flickr"}) + `\b([0-9a-z]{32})\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{"flickr"}
}
// FromData will find and optionally verify Flickr 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Flickr,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://www.flickr.com/services/rest/?method=flickr.tags.getHotList&api_key=%s", resMatch), nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
continue
}
body := string(bodyBytes)
if (res.StatusCode >= 200 && res.StatusCode < 300) && strings.Contains(body, "owner=") {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Flickr
}
func (s Scanner) Description() string {
return "Flickr is an image and video hosting service. Flickr API keys can be used to access and modify user data and perform various operations within the Flickr ecosystem."
}
================================================
FILE: pkg/detectors/flickr/flickr_integration_test.go
================================================
//go:build detectors
// +build detectors
package flickr
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFlickr_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FLICKR")
inactiveSecret := testSecrets.MustGetField("FLICKR_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flickr secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Flickr,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flickr secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Flickr,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Flickr.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Flickr.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/flickr/flickr_test.go
================================================
package flickr
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Flickr",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"flickr_secret": "x0b3lyve4dzszjak9afwb1bp3bz9z4z3"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "x0b3lyve4dzszjak9afwb1bp3bz9z4z3"
)
func TestFlickr_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/flightapi/flightapi.go
================================================
package flightapi
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"flightapi"}) + `\b([a-z0-9]{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{"flightapi"}
}
// FromData will find and optionally verify FlightApi 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FlightApi,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.flightapi.io/iata/%s/london/airport", resMatch), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FlightApi
}
func (s Scanner) Description() string {
return "FlightApi is a service used for accessing flight-related data. FlightApi keys can be used to query flight information and other related services."
}
================================================
FILE: pkg/detectors/flightapi/flightapi_integration_test.go
================================================
//go:build detectors
// +build detectors
package flightapi
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFlightApi_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FLIGHTAPI")
inactiveSecret := testSecrets.MustGetField("FLIGHTAPI_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flightapi secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FlightApi,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flightapi secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FlightApi,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("FlightApi.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("FlightApi.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/flightapi/flightapi_test.go
================================================
package flightapi
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "FlightAPI",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"flightapi_secret": "024j4wjk6671d9kvm8a7iouu"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "024j4wjk6671d9kvm8a7iouu"
)
func TestFlightAPI_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/flightlabs/flightlabs.go
================================================
package flightlabs
import (
"context"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(`\b(eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9\.ey[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]{86})\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{"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9"}
}
func (s Scanner) getClient() *http.Client {
client := s.client
if client == nil {
client = defaultClient
}
return client
}
// FromData will find and optionally verify FlightLabs 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)
uniqueKeys := make(map[string]struct{})
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueKeys[match[1]] = struct{}{}
}
for key := range uniqueKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FlightLabs,
Raw: []byte(key),
}
if verify {
isVerified, verificationErr := verifyMatch(ctx, s.getClient(), key)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, key)
}
results = append(results, s1)
}
return results, nil
}
func verifyMatch(ctx context.Context, client *http.Client, secret string) (bool, error) {
// API Reference: https://www.goflightlabs.com/airports-by-filters
url := fmt.Sprintf("https://www.goflightlabs.com/airports-by-filter?access_key=%s&iata_code=JFK", secret)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return false, err
}
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.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FlightLabs
}
func (s Scanner) Description() string {
return "FlightLabs provides a comprehensive API for accessing real-time and historical flight data. The API keys can be used to query flight information, schedules, and other related data."
}
================================================
FILE: pkg/detectors/flightlabs/flightlabs_integration_test.go
================================================
//go:build detectors
// +build detectors
package flightlabs
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFlightLabs_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FLIGHTLABS")
inactiveSecret := testSecrets.MustGetField("FLIGHTLABS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flightlabs secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FlightLabs,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flightlabs secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FlightLabs,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("FlightLabs.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("FlightLabs.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/flightlabs/flightlabs_test.go
================================================
package flightlabs
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "FlightLabs",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"flightlabs_secret": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.ey3UspgSVM9j311NMff2N27tGEWSmr8sl1SguOxwzelJSYPOOVp-8BwHsdHqWKWpoVvZAc4kXKJ2kpROZ1RY_0xSj51iWOoi5UvvxOlaIHTzMEEiudOJRQuzYxwtqtl1rZyRlFuxTm0YR5wWPFM0GlWzmCf_yKz.atNcL556uLcZ9D6MTIlQoC9hD1u3EbBqL6nb32cgFowGosYnqkSgbCFPLg6LIhK_PADfDzUY2bTEsk7uEIbGxP"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.ey3UspgSVM9j311NMff2N27tGEWSmr8sl1SguOxwzelJSYPOOVp-8BwHsdHqWKWpoVvZAc4kXKJ2kpROZ1RY_0xSj51iWOoi5UvvxOlaIHTzMEEiudOJRQuzYxwtqtl1rZyRlFuxTm0YR5wWPFM0GlWzmCf_yKz.atNcL556uLcZ9D6MTIlQoC9hD1u3EbBqL6nb32cgFowGosYnqkSgbCFPLg6LIhK_PADfDzUY2bTEsk7uEIbGxP"
)
func TestFlightLabs_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/flightstats/flightstats.go
================================================
package flightstats
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"flightstats"}) + `\b([0-9a-z]{8})\b`)
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"flightstats"}) + `\b([0-9a-z]{32})\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{"flightstats"}
}
// FromData will find and optionally verify Flightstats 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
resId := strings.TrimSpace(idMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Flightstats,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.flightstats.com/flex/aircraft/rest/v1/json/availableFields?appId=%s&appKey=%s", resId, resMatch), nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
continue
}
body := string(bodyBytes)
validResponse := (res.StatusCode >= 200 && res.StatusCode < 300 && strings.Contains(body, "id")) || (res.StatusCode == 403 && strings.Contains(body, "application is not active"))
if validResponse {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Flightstats
}
func (s Scanner) Description() string {
return "Flightstats provides APIs for accessing flight data and statistics. Flightstats API keys can be used to retrieve and manipulate flight-related information."
}
================================================
FILE: pkg/detectors/flightstats/flightstats_integration_test.go
================================================
//go:build detectors
// +build detectors
package flightstats
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFlightstats_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
id := testSecrets.MustGetField("FLIGHTSTATS_ID")
secret := testSecrets.MustGetField("FLIGHTSTATS_KEY")
inactiveSecret := testSecrets.MustGetField("FLIGHTSTATS_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flightstats secret %s within flightstats id %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Flightstats,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flightstats secret %s within flightstats id %s but not valid", inactiveSecret, id)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Flightstats,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Flightstats.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Flightstats.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/flightstats/flightstats_test.go
================================================
package flightstats
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "FlightStats",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"flightstats_id":"35o5omng",
"flightstats_secret": "ksqxv0hkdkli9s71bd7ebfl5cijbab7f"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "ksqxv0hkdkli9s71bd7ebfl5cijbab7f"
)
func TestFlightStats_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/float/float.go
================================================
package float
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"float"}) + `\b([a-f0-9]{16}[A-Za-z0-9+/]{42,43}=)`)
)
// 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{"float"}
}
// FromData will find and optionally verify Float 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Float,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.float.com/v3/people", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
req.Header.Add("User-Agent", "TruffleHog3 (example@example.com)")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Float
}
func (s Scanner) Description() string {
return "Float is a resource management software used for planning and scheduling projects. Float API keys can be used to access and modify project data and schedules."
}
================================================
FILE: pkg/detectors/float/float_integration_test.go
================================================
//go:build detectors
// +build detectors
package float
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFloat_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FLOAT_TOKEN")
inactiveSecret := testSecrets.MustGetField("FLOAT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a float secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Float,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a float secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Float,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Float.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Float.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/float/float_test.go
================================================
package float
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Float",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"float_secret": "50604f993cb9e4dfCsmIjdN5bCx5FnnfaukUdv7S9sm9L5wB2fZSUkZqHn="
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "50604f993cb9e4dfCsmIjdN5bCx5FnnfaukUdv7S9sm9L5wB2fZSUkZqHn="
)
func TestFloat_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/flowflu/flowflu.go
================================================
package flowflu
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"flowflu"}) + `\b([a-zA-Z0-9]{51})\b`)
accountPat = regexp.MustCompile(detectors.PrefixRegex([]string{"flowflu", "account"}) + `\b([a-zA-Z0-9]{4,30})\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{"flowflu"}
}
// FromData will find and optionally verify FlowFlu 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
accountMatches := accountPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, accountMatch := range accountMatches {
resAccount := strings.TrimSpace(accountMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FlowFlu,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://%s.flowlu.com/api/v1/module/crm/lead/list?api_key=%s", resAccount, resMatch), nil)
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
continue
}
bodyString := string(bodyBytes)
validResponse := strings.Contains(bodyString, `total_result`)
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
if validResponse {
s1.Verified = true
} else {
s1.Verified = false
}
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FlowFlu
}
func (s Scanner) Description() string {
return "FlowFlu is a service used for managing customer relationships and projects. FlowFlu API keys can be used to access and manipulate CRM data."
}
================================================
FILE: pkg/detectors/flowflu/flowflu_integration_test.go
================================================
//go:build detectors
// +build detectors
package flowflu
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFlowFlu_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
account := testSecrets.MustGetField("FLOWFLU_ACCOUNT")
secret := testSecrets.MustGetField("FLOWFLU_TOKEN")
inactiveSecret := testSecrets.MustGetField("FLOWFLU_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flowflu secret %s within flowflu account %s", secret, account)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FlowFlu,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flowflu secret %s within flowflu account %s but not valid", inactiveSecret, account)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FlowFlu,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("FlowFlu.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("FlowFlu.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/flowflu/flowflu_test.go
================================================
package flowflu
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "FlowFlu",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"flowflu_account": "tsPX0KAuOZPMy9BMjTwmph",
"flowflu_secret": "QdUZ0jRet5Z8nQjMgbLUGHZqShpFHCydCnL7hpTNXnwpUy75SJi"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secrets = []string{
"QdUZ0jRet5Z8nQjMgbLUGHZqShpFHCydCnL7hpTNXnwpUy75SJi",
"QdUZ0jRet5Z8nQjMgbLUGHZqShpFHCydCnL7hpTNXnwpUy75SJi",
}
)
func TestFlowFlu_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: secrets,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/flutterwave/flutterwave.go
================================================
package flutterwave
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(FLWSECK-[0-9a-z]{32}-X)\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{"FLWSECK-"}
}
// FromData will find and optionally verify Flutterwave 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Flutterwave,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.flutterwave.com/v3/subaccounts", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Flutterwave
}
func (s Scanner) Description() string {
return "Flutterwave is a payment technology company providing seamless and secure payment solutions for businesses. Flutterwave API keys can be used to access and manage payment services and transactions."
}
================================================
FILE: pkg/detectors/flutterwave/flutterwave_integration_test.go
================================================
//go:build detectors
// +build detectors
package flutterwave
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFlutterwave_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FLUTTERWAVE_TOKEN")
inactiveSecret := testSecrets.MustGetField("FLUTTERWAVE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flutterwave secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Flutterwave,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flutterwave secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Flutterwave,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Flutterwave.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Flutterwave.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/flutterwave/flutterwave_test.go
================================================
package flutterwave
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "FlutterWave",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"flutterwave_secret": "FLWSECK-aylhdv2oo3wf5tylj8s4d9bqb8adoebx-X"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "FLWSECK-aylhdv2oo3wf5tylj8s4d9bqb8adoebx-X"
)
func TestFlutterWave_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/flyio/flyio.go
================================================
package flyio
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct {
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(FlyV1 fm\d+_[A-Za-z0-9+\/=,_-]{500,700})\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{"FlyV1"}
}
// FromData will find and optionally verify Flyio 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 match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FlyIO,
Raw: []byte(match),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := verifyMatch(ctx, client, match)
s1.Verified = isVerified
if verificationErr != nil {
s1.SetVerificationError(verificationErr, match)
}
}
results = append(results, s1)
}
return
}
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) {
// Not setting org_slug intentionally, as it's not required for the token to be valid.
// Initially, an organization named "personal" is created by FlyIO when the user signs up for an account. We cannot rely on this as it can be deleted.
// 403 is returned if incorrect org_slug is sent.
// 401 is returned if the token is invalid.
// 400 is returned if the token is valid but no org_slug is sent.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.machines.dev/v1/apps?org_slug=", http.NoBody)
if err != nil {
return false, nil
}
req.Header.Add("accept", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", 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.StatusBadRequest:
// Not setting org_slug returns a 400 error, which is expected.
return true, nil
case http.StatusUnauthorized:
// The secret is determinately not verified (nothing to do)
return false, nil
default:
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
return false, err
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FlyIO
}
func (s Scanner) Description() string {
return "Fly.io is a platform for running applications globally. Fly.io tokens can be used to access the Fly.io API and manage applications."
}
func (s Scanner) IsFalsePositive(result detectors.Result) (bool, string) {
// ignore AAAAAA for Flyio detector
if strings.Contains(string(result.Raw), "AAAAAA") {
return false, ""
}
// For non-matching patterns, fall back to default false positive logic
return detectors.IsKnownFalsePositive(string(result.Raw), detectors.DefaultFalsePositives, true)
}
================================================
FILE: pkg/detectors/flyio/flyio_integration_test.go
================================================
//go:build detectors
// +build detectors
package flyio
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/detectorspb"
)
func TestFlyio_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FLYIO")
inactiveSecret := testSecrets.MustGetField("FLYIO_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",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flyio secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FlyIO,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flyio secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FlyIO,
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 flyio secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FlyIO,
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 flyio secret %s within", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_FlyIO,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 404"))
return []detectors.Result{r}
}(),
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("Flyio.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.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "AnalysisInfo")
ignoreUnexported := cmpopts.IgnoreUnexported(detectors.Result{})
if diff := cmp.Diff(got, tt.want, ignoreOpts, ignoreUnexported); diff != "" {
t.Errorf("Flyio.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
}
})
}
}
================================================
FILE: pkg/detectors/flyio/flyio_test.go
================================================
package flyio
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFlyio_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "typical pattern",
input: "flyio_token = 'FlyV1 fm2_AD1shwGbLSpZSPEXM1vhcbPZowurCDkXySOOJj0w4G2abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'",
want: []string{"FlyV1 fm2_AD1shwGbLSpZSPEXM1vhcbPZowurCDkXySOOJj0w4G2abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"},
},
{
name: "invalid pattern - too short",
input: "flyio_token = 'FlyV1 fm2_short'",
want: []string{},
},
{
name: "invalid pattern - wrong prefix",
input: "flyio_token = 'FlyV2 fm2_AD1shwGbLSpZSPEXM1vhcbPZowurCDkXySOOJj0w4G2abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'",
want: []string{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(test.want) > 0 && 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 && len(test.want) > 0 {
t.Errorf("did not receive result")
} else if len(results) > 0 && len(test.want) == 0 {
t.Errorf("expected no results, but received %d", len(results))
} 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)
}
})
}
}
func TestFlyio_IsFalsePositive(t *testing.T) {
s := Scanner{}
tests := []struct {
name string
token string
expected bool
reason string
}{
{
name: "token with AAAAAA - should not be flagged as false positive",
token: "FlyV1 fm2_abcdAAAAAA1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890",
expected: false,
reason: "",
},
{
name: "token with example pattern - should be false positive",
token: "FlyV1 fm2_1234example567890zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321",
expected: true,
reason: "contains term: example",
},
{
name: "token with sample pattern - should be false positive",
token: "FlyV1 fm2_1234sample567890zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321",
expected: true,
reason: "contains term: sample",
},
{
name: "token with xxxxxx pattern - should be false positive",
token: "FlyV1 fm2_1234xxxxxx567890zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA0987654321",
expected: true,
reason: "contains term: xxxxxx",
},
{
name: "valid token without AAAAAA - should not be false positive",
token: "FlyV1 fm2_1234567890zyxwvutsrqponmlkjihgfZYXWVUTSRQPONMLKJIHGF0987654321zyxwvutsrqponmlkjihgfZYXWVUTSRQPONMLKJIHGF0987654321zyxwvutsrqponmlkjihgfZYXWVUTSRQPONMLKJIHGF0987654321zyxwvutsrqponmlkjihgfZYXWVUTSRQPONMLKJIHGF0987654321zyxwvutsrqponmlkjihgfZYXWVUTSRQPONMLKJIHGF0987654321zyxwvutsrqponmlkjihgfZYXWVUTSRQPONMLKJIHGF0987654321zyxwvutsrqponmlkjihgfZYXWVUTSRQPONMLKJIHGF0987654321zyxwvutsrqponmlkjihgfZYXWVUTSRQPONMLKJIHGF0987654321zyxwvutsrqponmlkjihgfZYXWVUTSRQPONMLKJIHGF0987654321",
expected: false,
reason: "",
},
{
name: "regular string without pattern - should not be false positive",
token: "XYZABC123789def456",
expected: false,
reason: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := detectors.Result{
DetectorType: detectorspb.DetectorType_FlyIO,
Raw: []byte(tt.token),
}
isFP, reason := s.IsFalsePositive(result)
if isFP != tt.expected {
t.Errorf("IsFalsePositive() got = %v, want %v (reason: %s)", isFP, tt.expected, reason)
}
if tt.expected && reason != tt.reason {
t.Errorf("IsFalsePositive() reason got = %v, want %v", reason, tt.reason)
}
})
}
}
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)
}
}
})
}
}
================================================
FILE: pkg/detectors/fmfw/fmfw.go
================================================
package fmfw
import (
"context"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"fmfw"}) + `\b([a-zA-Z0-9_-]{32})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"fmfw"}) + `\b([a-zA-Z0-9-]{32})\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{"fmfw"}
}
// FromData will find and optionally verify Fmfw 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
tokenPatMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
userPatMatch := strings.TrimSpace(idMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Fmfw,
Raw: []byte(tokenPatMatch),
}
if verify {
timeout := 10 * time.Second
client.Timeout = timeout
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.fmfw.io/api/3/spot/balance", nil)
if err != nil {
continue
}
req.SetBasicAuth(userPatMatch, tokenPatMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Fmfw
}
func (s Scanner) Description() string {
return "FMFW is a cryptocurrency exchange platform. FMFW API keys can be used to access and manage account data and perform trading operations."
}
================================================
FILE: pkg/detectors/fmfw/fmfw_integration_test.go
================================================
//go:build detectors
// +build detectors
package fmfw
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFmfw_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FMFW")
user := testSecrets.MustGetField("FMFW_USER")
inactiveSecret := testSecrets.MustGetField("FMFW_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a fmfw secret %s within fmfw %s", secret, user)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Fmfw,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a fmfw secret %s within fmfw %s but not valid", inactiveSecret, user)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Fmfw,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Fmfw.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Fmfw.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/fmfw/fmfw_test.go
================================================
package fmfw
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "FMFW",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"fmfw_key": "cno3jaTTtgBeo3b_y82FPdrw4Yxfspvd",
"fmfw_id": "nsrD8XVjeXc4Z-uGw6CgTBRXHmTjbizL"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secrets = []string{
"cno3jaTTtgBeo3b_y82FPdrw4Yxfspvd",
"nsrD8XVjeXc4Z-uGw6CgTBRXHmTjbizL",
}
)
func TestFmFw_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: secrets,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/formbucket/formbucket.go
================================================
package formbucket
import (
"context"
"fmt"
"io"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"formbucket"}) + `\b([0-9A-Za-z]{1,}.[0-9A-Za-z]{1,}\.[0-9A-Z-a-z\-_]{1,})`)
)
// 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{"formbucket"}
}
// FromData will find and optionally verify FormBucket 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FormBucket,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.formbucket.com/v1/profile", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
body, errBody := io.ReadAll(res.Body)
if errBody != nil {
continue
}
bodyString := string(body)
validResponse := strings.Contains(bodyString, `created_on`)
defer res.Body.Close()
if errBody == nil {
if res.StatusCode >= 200 && res.StatusCode < 300 && validResponse {
s1.Verified = true
}
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Description() string {
return "FormBucket is a service used to collect and manage form submissions. The detected credential can be used to access and modify form data."
}
type Response struct {
Anonymous bool `json:"anonymous"`
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FormBucket
}
================================================
FILE: pkg/detectors/formbucket/formbucket_integration_test.go
================================================
//go:build detectors
// +build detectors
package formbucket
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFormBucket_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FORMBUCKET")
inactiveSecret := testSecrets.MustGetField("FORMBUCKET_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a formbucket secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FormBucket,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a formbucket secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FormBucket,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("FormBucket.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("FormBucket.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/formbucket/formbucket_test.go
================================================
package formbucket
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "FormBucket",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"formbucket_secret": "qE4P6YytmrnbI4o7xmr3Ct9umMOgM7CKqlUTvdMsXICpUEEow2ZDQi0CyZ7AYir4BkqsxvKdV33095olnQO6gkHgoZsSHPG41oqLrrM3g.l1Vt_Jv9iuT7w4si"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "qE4P6YytmrnbI4o7xmr3Ct9umMOgM7CKqlUTvdMsXICpUEEow2ZDQi0CyZ7AYir4BkqsxvKdV33095olnQO6gkHgoZsSHPG41oqLrrM3g.l1Vt_Jv9iuT7w4si"
)
func TestFormBucket_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/formcraft/formcraft.go
================================================
package formcraft
import (
"context"
b64 "encoding/base64"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"formcraft"}) + `\b([0-9a-z]{16})\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{"formcraft"}
}
// FromData will find and optionally verify Formcraft 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Formcraft,
Raw: []byte(resMatch),
}
if verify {
data := fmt.Sprintf("%s:", resMatch)
sEnc := b64.StdEncoding.EncodeToString([]byte(data))
req, err := http.NewRequestWithContext(ctx, "GET", "https://formcrafts.com/api/v1/", nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Formcraft
}
func (s Scanner) Description() string {
return "Formcraft is a form building and data collection service. Formcraft keys can be used to access and manage forms and collected data."
}
================================================
FILE: pkg/detectors/formcraft/formcraft_integration_test.go
================================================
//go:build detectors
// +build detectors
package formcraft
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFormcraft_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FORMCRAFT")
inactiveSecret := testSecrets.MustGetField("FORMCRAFT_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a formcraft secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Formcraft,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a formcraft secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Formcraft,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Formcraft.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Formcraft.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/formcraft/formcraft_test.go
================================================
package formcraft
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "FormCraft",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"formcraft_secret": "zgej8qae3ehc0mjo"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "zgej8qae3ehc0mjo"
)
func TestFormCraft_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/formio/formio.go
================================================
package formio
import (
"context"
"net/http"
"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/detectorspb"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"formio"}) + `\b(eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[0-9A-Za-z]{220,310}\.[0-9A-Z-a-z\-_]{43}[ \r\n]{1})`)
)
// 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{"formio"}
}
// FromData will find and optionally verify FormIO 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FormIO,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://formio.form.io/current", nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("x-jwt-token", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FormIO
}
func (s Scanner) Description() string {
return "FormIO is a platform for building form-based applications. FormIO JWT tokens can be used to authenticate and interact with FormIO services."
}
================================================
FILE: pkg/detectors/formio/formio_integration_test.go
================================================
//go:build detectors
// +build detectors
package formio
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFormIO_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FORMIO")
inactiveSecret := testSecrets.MustGetField("FORMIO_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a formio secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FormIO,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a formio secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FormIO,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("FormIO.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("FormIO.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++ {
s.FromData(ctx, false, data)
}
})
}
}
================================================
FILE: pkg/detectors/formio/formio_test.go
================================================
package formio
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "FormIO",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"formio_secret": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.3IJk8Ys67c6tWlZi346ptymjjgkzwSyE5G2RbPS3kNyxuD4DFUj1vJFqlzZUTwUTHzhTEiUCPG3xtBFPfEBCGBtKDdh4SB3QhWHZAvEx3v61Mv1bsg3dhiKeGEJBluxNr8FRWHNmCaWq7KQpqK6YDX7ItacPKYKzOWXw16Swwj8lnKORhut3TjIsNa0dSoTCGeVZQey0RD0GuWuuXIz5Bu6xQoVnexXGKmbm3wu4VMxsXaquKvW6xXo.lQWeje6Ck-SNJR1LEwHqOFjVfad7-SXyV2nivyHnpxt "
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.3IJk8Ys67c6tWlZi346ptymjjgkzwSyE5G2RbPS3kNyxuD4DFUj1vJFqlzZUTwUTHzhTEiUCPG3xtBFPfEBCGBtKDdh4SB3QhWHZAvEx3v61Mv1bsg3dhiKeGEJBluxNr8FRWHNmCaWq7KQpqK6YDX7ItacPKYKzOWXw16Swwj8lnKORhut3TjIsNa0dSoTCGeVZQey0RD0GuWuuXIz5Bu6xQoVnexXGKmbm3wu4VMxsXaquKvW6xXo.lQWeje6Ck-SNJR1LEwHqOFjVfad7-SXyV2nivyHnpxt"
)
func TestFormIO_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/formsite/formsite.go
================================================
package formsite
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"formsite"}) + `\b([a-zA-Z0-9]{32})\b`)
serverPat = regexp.MustCompile(detectors.PrefixRegex([]string{"formsite"}) + `\b(fs[0-9]{1,4})\b`)
userPat = regexp.MustCompile(detectors.PrefixRegex([]string{"formsite"}) + `\b([a-zA-Z0-9]{6})\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{"formsite"}
}
// FromData will find and optionally verify Formsite 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
serverMatches := serverPat.FindAllStringSubmatch(dataStr, -1)
userMatches := userPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, serverMatch := range serverMatches {
resServerMatch := strings.TrimSpace(serverMatch[1])
for _, userMatch := range userMatches {
resUserMatch := strings.TrimSpace(userMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Formsite,
Raw: []byte(resMatch),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://%s.formsite.com/api/v2/%s/forms", resServerMatch, resUserMatch), nil)
if err != nil {
continue
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Formsite
}
func (s Scanner) Description() string {
return "Formsite is an online form builder service. Formsite API keys can be used to access and manage forms and data submissions."
}
================================================
FILE: pkg/detectors/formsite/formsite_integration_test.go
================================================
//go:build detectors
// +build detectors
package formsite
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFormsite_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FORMSITE")
inactiveSecret := testSecrets.MustGetField("FORMSITE_INACTIVE")
server := testSecrets.MustGetField("FORMSITE_SERVER")
user := testSecrets.MustGetField("FORMSITE_USER")
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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a formsite secret %s within formsite server %s formsite user %s", secret, server, user)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Formsite,
Verified: true,
},
{
DetectorType: detectorspb.DetectorType_Formsite,
Verified: false,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a formsite secret %s within but not valid formsite server %s formsite user %s", inactiveSecret, server, user)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_Formsite,
Verified: false,
},
{
DetectorType: detectorspb.DetectorType_Formsite,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Formsite.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Formsite.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/formsite/formsite_test.go
================================================
package formsite
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "Formsite",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"formsite_server": "fs02",
"formsite_user": "ITest2",
"formsite_secret": "8PKXsB1ohFUGnw0j8y3g9pRUvDj0I1Ha"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "8PKXsB1ohFUGnw0j8y3g9pRUvDj0I1Ha"
)
func TestFormsite_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: []string{secret},
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/foursquare/foursquare.go
================================================
package foursquare
import (
"context"
"fmt"
"net/http"
"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/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"foursquare"}) + `\b([0-9A-Z]{48})\b`)
secretMatch = regexp.MustCompile(detectors.PrefixRegex([]string{"foursquare"}) + `\b([0-9A-Z]{48})\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{"foursquare"}
}
// FromData will find and optionally verify Foursquare 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)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
secretMatches := secretMatch.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
for _, secretMatch := range secretMatches {
resSecret := strings.TrimSpace(secretMatch[1])
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FourSquare,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resSecret),
}
if verify {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.foursquare.com/v2/venues/trending?client_id=%s&client_secret=%s&v=20211019&near=LA", resMatch, resSecret), nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
}
results = append(results, s1)
}
}
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FourSquare
}
func (s Scanner) Description() string {
return "Foursquare is a technology company that uses location intelligence to build meaningful consumer experiences and business solutions. Foursquare API keys can be used to access and interact with their services."
}
================================================
FILE: pkg/detectors/foursquare/foursquare_integration_test.go
================================================
//go:build detectors
// +build detectors
package foursquare
import (
"context"
"fmt"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestFoursquare_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
id := testSecrets.MustGetField("FOURSQUARE")
secret := testSecrets.MustGetField("FOURSQUARE_SECRET")
inactiveId := testSecrets.MustGetField("FOURSQUARE_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: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a foursquare secret %s within foursquare id %s", secret, id)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FourSquare,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a foursquare secret %s within foursquare id %s but not valid", secret, inactiveId)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FourSquare,
Verified: false,
},
},
wantErr: 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Foursquare.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])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Foursquare.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)
}
}
})
}
}
================================================
FILE: pkg/detectors/foursquare/foursquare_test.go
================================================
package foursquare
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = `[{
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
"name": "FormSquare",
"type": "Detector",
"api": true,
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"foursquare_key": "NUAL8SFC7DGMAA0V79J57TSMOVZTH5HI5B7IM1BCF4L3IQXG",
"foursquare_secret": "CII2183RFI60TZGXHK25A06VWZYE19IBMGJQZAIG3PPJFZ6M"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secrets = []string{
"CII2183RFI60TZGXHK25A06VWZYE19IBMGJQZAIG3PPJFZ6MCII2183RFI60TZGXHK25A06VWZYE19IBMGJQZAIG3PPJFZ6M",
"NUAL8SFC7DGMAA0V79J57TSMOVZTH5HI5B7IM1BCF4L3IQXGCII2183RFI60TZGXHK25A06VWZYE19IBMGJQZAIG3PPJFZ6M",
"CII2183RFI60TZGXHK25A06VWZYE19IBMGJQZAIG3PPJFZ6MNUAL8SFC7DGMAA0V79J57TSMOVZTH5HI5B7IM1BCF4L3IQXG",
"NUAL8SFC7DGMAA0V79J57TSMOVZTH5HI5B7IM1BCF4L3IQXGNUAL8SFC7DGMAA0V79J57TSMOVZTH5HI5B7IM1BCF4L3IQXG",
}
)
func TestFourSquare_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: validPattern,
want: secrets,
},
}
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)
}
})
}
}
================================================
FILE: pkg/detectors/fp_badlist.txt
================================================
value
from
array
uint
boolean
config
parse
func
param
cancel
export
substr
name
utils
token
data
encode
else
auth
define
space
ident
block
type
index
case
safe
decrypt
event
message
args
head
cookie
buffer
return
throw
derive
bits
bytes
node
code
const
logger
hash
source
tuple
this
style
version
batch
enable
disable
remove
port
modifi
kind
sort
format
text
numer
punct
litera
context
layer
delta
init
final
color
cancel
calc
append
slice
force
escape
exit
frame
char
align
implem
keyword
trace
truncate
group
href
scale
model
visual
model
never
win32
goto
small
large
lexer
replace
variab
close
defer
start
;var
storage
blob
cred
math
.xml
conflict
hack
package
contract
schema
vec<
ed25519
prefix
suffix
compress
hmac
sha256
request
base
rest
session
spec
date
time
cache
build
check
install
asset
vendor
x509
usage
errno
crypto
sha384
sha512
sha1
ecdsa
algo
cert
progress
marshal
strconv
primary
unseal
pkcs
recurs
struct
entry
vault:v
lease
share
mouse
press
public
cloud
amq.gem
resp
ring
error
revoke
encrypt
binary
2018-
2019-
2020-
2021-
2022-
byte
root
readon
test
2048
match
private
key_
aes256
aes128
state
alloc
proto
term
server
step
limit
backend
len(
increm
bucket
object
last
first
start
stop
seal
transit
offset
pointer
arr[
cluster
value
read
sign
leave
lock
part
central
local
http:
https:
delete
insert
append
table
mutat
colon
bound
{key
valid
proc
enum
query
open
module
program
import
pinned
pubexp
keygen
shim
expr
buf[
keyid
.key
keys
queue
sha-1
sha-256
sha-512
sha-384
user
info
[idx
gray
black
white
yellow
orange
purple
=val
key=
policy
field
json
piece
depth
label
daemon
cron
uuid
k8s.
role
application
explic
random
DES3
3DES
amq.gen
xml:"
tag:
.Get
.Put
.Delete
extend
split
option
fontsize
"
keyboard
custom
item
emulate
iphone
develop
master
slave
secondary
example
================================================
FILE: pkg/detectors/fp_programmingbooks.txt
================================================
--+--+--
-->
$${balance
$curry(...args
${email
$.getjson(url
$ident
$('
add(multiply(x
addnextgrade
add-one
add_one
addpage
address
add
add(self
add
addstudents
add/target
addten
add_text
add-two
adequate
adhere
adjust
administrative
admirable
admonition
advanced
advent
advertising
advice
advisable
aesthetic
affect
aforementioned
afraid
a.get('team
aggregates
aimlessly
a.iter
ajaxcall
ajax(url,cb
alarms!
albeit
albert
algebra
algorithm
all.empty
all(false
allocate
all-or-nothing
allthechildren
all(true
allupper
almost
alphabetic
already
alt="a
although
altogether
_always_
always
amalgamations
a'].map($
amateur
amazement
amazing
ambiguity
ambition
amd-style
amenable
amending
amidst
amoduleineed
amount
ampersand
analog
analysis
anarchy
anatomy
@anaufalm
ancestor
anchor
ancients
and/or
andrew
andthisonetoo
angels
animal
aniston
annihilate
annotate
announcement
annoyed
anonymous
another
answer
anti-class
anxious
any.empty
any(false
anyfunctor
anyhow
anymore
anyone
anything
any(true
anyway
anywhere
a.of(f
apiendpoints
api.flickr.com
ap`ing
apostrophe
ap(other
apparent
app('cats
appeal
append
appetizer
apple”
applicability
appreciate
approach
app
arabic
arbitrarily
arcane
architectural
arc>
arc
area(&self
aren't
aren’t
args.length
argstr
arguably
arguing
argument
arithmetic
armstrong
around
arrangement
arrcopy
arr.entries
arrived
arr.length
art4thesould
arthur
article
artifact
artist
as_bytes
ashamed
asking
askquestion
asm.js
as_mut_ptr
a`
aspect
as_ref
assemble
assert!
assign
assist
associate
‘associated
assortment
assume
assure
asterisk
ast.ident
astound
ast)—to
atomic
attach
attempt
attend
attitude
attractions
attribute
audience
audited
augments
august
austin
authenticate
author
auto-complete
autocompletion
automate
autosave
available
a_value
average
awesome!
awhile
awkward
azure
b${str
babylonians
baby_name
backed
background
backing
back_of_house
backported
backspace
back-tick
backtrace
backward
badidea
bahasa
balance
balloons!
banana
bandwagoning
b)).ap(f
bar...it
barren
barrier
bartenders
baseless
basename
bathwater
battle
bearing
beatnik
beautiful
became
because
become
been!—should
before
#beginners
behalf
behave
behind
behold
belabor
believe
belong
bending
beneath
beneficial
benkort
besides
betrays
better
between
beware
beyond
bibliography
bigger
bigint
big-integer
bikeshedding
billion
binaries
binding
bind(this
bioinformatics
bitand
bitten
bitwise
bitxor
bizarrely
bjarne
blaring
bleeding
blogcontroller
blog({}).fork
blogpage
blog’s
bloody
bludgeon
blurp_blurp
boasting
bodies
body)?
boiler
boldly
bolster
bonafide
--book
bookkeeping
book
book's
boolean
boring
borrow
bothering
both_float
bottle
bottom
bounce
box